Default type map for YAML deserialization (#599)

* Affects Yaml.LoadAllFrom* methods
* Doesn't require user to explicitly pass a mapping for known types
* Allows user to specify a mapping for custom types, if required
This commit is contained in:
Benjamin Burns
2021-04-06 07:43:19 +12:00
committed by GitHub
parent edaeb1443a
commit 57aef03afe
2 changed files with 140 additions and 26 deletions

View File

@@ -39,6 +39,19 @@ namespace k8s
.WithOverridesFromJsonPropertyAttributes() .WithOverridesFromJsonPropertyAttributes()
.BuildValueSerializer(); .BuildValueSerializer();
private static readonly IDictionary<string, Type> ModelTypeMap = typeof(KubernetesEntityAttribute).Assembly
.GetTypes()
.Where(t => t.GetCustomAttributes(typeof(KubernetesEntityAttribute), true).Any())
.ToDictionary(
t =>
{
var attr = (KubernetesEntityAttribute)t.GetCustomAttribute(
typeof(KubernetesEntityAttribute), true);
var groupPrefix = string.IsNullOrEmpty(attr.Group) ? "" : $"{attr.Group}/";
return $"{groupPrefix}{attr.ApiVersion}/{attr.Kind}";
},
t => t);
public class ByteArrayStringYamlConverter : IYamlTypeConverter public class ByteArrayStringYamlConverter : IYamlTypeConverter
{ {
public bool Accepts(Type type) public bool Accepts(Type type)
@@ -84,10 +97,11 @@ namespace k8s
/// The stream to load the objects from. /// The stream to load the objects from.
/// </param> /// </param>
/// <param name="typeMap"> /// <param name="typeMap">
/// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod) /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will
/// be used.
/// </param> /// </param>
/// <returns>collection of objects</returns> /// <returns>collection of objects</returns>
public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, Dictionary<string, Type> typeMap) public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, IDictionary<string, Type> typeMap = null)
{ {
var reader = new StreamReader(stream); var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync().ConfigureAwait(false); var content = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -99,9 +113,12 @@ namespace k8s
/// Load a collection of objects from a file asynchronously /// Load a collection of objects from a file asynchronously
/// </summary> /// </summary>
/// <param name="fileName">The name of the file to load from.</param> /// <param name="fileName">The name of the file to load from.</param>
/// <param name="typeMap">A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod)</param> /// <param name="typeMap">
/// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will
/// be used.
/// </param>
/// <returns>collection of objects</returns> /// <returns>collection of objects</returns>
public static async Task<List<object>> LoadAllFromFileAsync(string fileName, Dictionary<string, Type> typeMap) public static async Task<List<object>> LoadAllFromFileAsync(string fileName, IDictionary<string, Type> typeMap = null)
{ {
using (var fileStream = File.OpenRead(fileName)) using (var fileStream = File.OpenRead(fileName))
{ {
@@ -116,15 +133,15 @@ namespace k8s
/// The string to load the objects from. /// The string to load the objects from.
/// </param> /// </param>
/// <param name="typeMap"> /// <param name="typeMap">
/// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod) /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will
/// be used.
/// </param> /// </param>
/// <returns>collection of objects</returns> /// <returns>collection of objects</returns>
public static List<object> LoadAllFromString(string content, Dictionary<string, Type> typeMap) public static List<object> LoadAllFromString(string content, IDictionary<string, Type> typeMap = null)
{ {
if (typeMap == null) var mergedTypeMap = new Dictionary<string, Type>(ModelTypeMap);
{ // merge in KVPs from typeMap, overriding any in ModelTypeMap
throw new ArgumentNullException(nameof(typeMap)); typeMap?.ToList().ForEach(x => mergedTypeMap[x.Key] = x.Value);
}
var types = new List<Type>(); var types = new List<Type>();
var parser = new Parser(new StringReader(content)); var parser = new Parser(new StringReader(content));
@@ -132,7 +149,7 @@ namespace k8s
while (parser.Accept<DocumentStart>(out _)) while (parser.Accept<DocumentStart>(out _))
{ {
var obj = Deserializer.Deserialize<KubernetesObject>(parser); var obj = Deserializer.Deserialize<KubernetesObject>(parser);
types.Add(typeMap[obj.ApiVersion + "/" + obj.Kind]); types.Add(mergedTypeMap[obj.ApiVersion + "/" + obj.Kind]);
} }
parser = new Parser(new StringReader(content)); parser = new Parser(new StringReader(content));

View File

@@ -23,11 +23,7 @@ kind: Namespace
metadata: metadata:
name: ns"; name: ns";
var types = new Dictionary<string, Type>(); var objs = Yaml.LoadAllFromString(content);
types.Add("v1/Pod", typeof(V1Pod));
types.Add("v1/Namespace", typeof(V1Namespace));
var objs = Yaml.LoadAllFromString(content, types);
Assert.Equal(2, objs.Count); Assert.Equal(2, objs.Count);
Assert.IsType<V1Pod>(objs[0]); Assert.IsType<V1Pod>(objs[0]);
Assert.IsType<V1Namespace>(objs[1]); Assert.IsType<V1Namespace>(objs[1]);
@@ -35,6 +31,36 @@ metadata:
Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name); Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name);
} }
#pragma warning disable CA1812 // Class is used for YAML deserialization tests
private class MyPod : V1Pod
{
}
#pragma warning restore CA1812
[Fact]
public void LoadAllFromStringWithTypes()
{
var types = new Dictionary<string, Type>();
types.Add("v1/Pod", typeof(MyPod));
var content = @"apiVersion: v1
kind: Pod
metadata:
name: foo
---
apiVersion: v1
kind: Namespace
metadata:
name: ns";
var objs = Yaml.LoadAllFromString(content, types);
Assert.Equal(2, objs.Count);
Assert.IsType<MyPod>(objs[0]);
Assert.IsType<V1Namespace>(objs[1]);
Assert.Equal("foo", ((MyPod)objs[0]).Metadata.Name);
Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name);
}
[Fact] [Fact]
public void LoadAllFromStringWithAdditionalProperties() public void LoadAllFromStringWithAdditionalProperties()
{ {
@@ -51,11 +77,7 @@ metadata:
name: ns name: ns
youDontKnow: Me"; youDontKnow: Me";
var types = new Dictionary<string, Type>(); var objs = Yaml.LoadAllFromString(content);
types.Add("v1/Pod", typeof(V1Pod));
types.Add("v1/Namespace", typeof(V1Namespace));
var objs = Yaml.LoadAllFromString(content, types);
Assert.Equal(2, objs.Count); Assert.Equal(2, objs.Count);
Assert.IsType<V1Pod>(objs[0]); Assert.IsType<V1Pod>(objs[0]);
Assert.IsType<V1Namespace>(objs[1]); Assert.IsType<V1Namespace>(objs[1]);
@@ -64,6 +86,33 @@ metadata:
Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name); Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name);
} }
[Fact]
public void LoadAllFromStringWithAdditionalPropertiesAndTypes()
{
var types = new Dictionary<string, Type>();
types.Add("v1/Pod", typeof(MyPod));
var content = @"apiVersion: v1
kind: Pod
metadata:
name: foo
namespace: ns
youDontKnow: Me
---
apiVersion: v1
kind: Namespace
metadata:
name: ns
youDontKnow: Me";
var objs = Yaml.LoadAllFromString(content, types);
Assert.Equal(2, objs.Count);
Assert.IsType<MyPod>(objs[0]);
Assert.IsType<V1Namespace>(objs[1]);
Assert.Equal("foo", ((MyPod)objs[0]).Metadata.Name);
Assert.Equal("ns", ((MyPod)objs[0]).Metadata.NamespaceProperty);
Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name);
}
[Fact] [Fact]
public async Task LoadAllFromFile() public async Task LoadAllFromFile()
{ {
@@ -77,9 +126,42 @@ kind: Namespace
metadata: metadata:
name: ns"; name: ns";
var tempFileName = Path.GetTempFileName();
try
{
await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(false);
var objs = await Yaml.LoadAllFromFileAsync(tempFileName).ConfigureAwait(false);
Assert.Equal(2, objs.Count);
Assert.IsType<V1Pod>(objs[0]);
Assert.IsType<V1Namespace>(objs[1]);
Assert.Equal("foo", ((V1Pod)objs[0]).Metadata.Name);
Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name);
}
finally
{
if (File.Exists(tempFileName))
{
File.Delete(tempFileName);
}
}
}
[Fact]
public async Task LoadAllFromFileWithTypes()
{
var types = new Dictionary<string, Type>(); var types = new Dictionary<string, Type>();
types.Add("v1/Pod", typeof(V1Pod)); types.Add("v1/Pod", typeof(MyPod));
types.Add("v1/Namespace", typeof(V1Namespace));
var content = @"apiVersion: v1
kind: Pod
metadata:
name: foo
---
apiVersion: v1
kind: Namespace
metadata:
name: ns";
var tempFileName = Path.GetTempFileName(); var tempFileName = Path.GetTempFileName();
try try
@@ -88,9 +170,9 @@ metadata:
var objs = await Yaml.LoadAllFromFileAsync(tempFileName, types).ConfigureAwait(false); var objs = await Yaml.LoadAllFromFileAsync(tempFileName, types).ConfigureAwait(false);
Assert.Equal(2, objs.Count); Assert.Equal(2, objs.Count);
Assert.IsType<V1Pod>(objs[0]); Assert.IsType<MyPod>(objs[0]);
Assert.IsType<V1Namespace>(objs[1]); Assert.IsType<V1Namespace>(objs[1]);
Assert.Equal("foo", ((V1Pod)objs[0]).Metadata.Name); Assert.Equal("foo", ((MyPod)objs[0]).Metadata.Name);
Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name); Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name);
} }
finally finally
@@ -131,6 +213,21 @@ metadata:
Assert.Equal("foo", obj.Metadata.Name); Assert.Equal("foo", obj.Metadata.Name);
} }
[Fact]
public void LoadFromStringWithAdditionalPropertiesAndCustomType()
{
var content = @"apiVersion: v1
kind: Pod
metadata:
name: foo
youDontKnow: Me
";
var obj = Yaml.LoadFromString<V1Pod>(content);
Assert.Equal("foo", obj.Metadata.Name);
}
[Fact] [Fact]
public void LoadNamespacedFromString() public void LoadNamespacedFromString()
{ {
@@ -442,7 +539,7 @@ spec:
var container = Assert.Single(obj.Spec.Containers); var container = Assert.Single(obj.Spec.Containers);
Assert.NotNull(container.Env); Assert.NotNull(container.Env);
var objStr = Yaml.SaveToString(obj); var objStr = Yaml.SaveToString(obj);
Assert.Equal(content, objStr); Assert.Equal(content.Replace("\r\n", "\n"), objStr.Replace("\r\n", "\n"));
} }
[Fact] [Fact]