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

[Off-topic] Example use of IYamlConvertible (custom/manual serialization of a class) #510

Open
xucian opened this issue Jul 4, 2020 · 0 comments

Comments

@xucian
Copy link

xucian commented Jul 4, 2020

Man, I really thank you for this amazing library. I have a complex structure of configs for a game project and this library is indispensable!
I didn't find any exhaustive example on how to use IYamlConvertible, so I dived in and tried different things. I thought it'd help others if I share my sample. Feel free to add it to the examples.

Basically, I have a "Design" config class which is pretty complex and I needed to create an "override node" class in order to replace any given node in the Design graph. Example: At path Design.Gameplay.Teams.MaxTeams there's an integer, and I may want to override that in certain setups, but without modifying the original Design object. This can be extended to overriding an entire subtree of properties (i.e. the Design.Gameplay.Teams property), not only a Scalar.

This is a good example where several concepts of IYamlConvertible are used and I think it's a good read for anyone interested.

[Serializable]
public class DesignDataNodeOverride : IYamlConvertible
{
	public string Path { get; set; } = "";
	public NodeSimpleTypeID ExpectedType { get; private set; } = new NodeSimpleTypeID(); // NodeSimpleTypeID is just a class that holds a Type's info (name and assembly name)
	public object Data { get; private set; } = null;


	public void SetDataAndInferType(object data)
	{
		Data = data;
		if (Data == null)
			ExpectedType = new NodeSimpleTypeID();
		else
			ExpectedType = new NodeSimpleTypeID(Data.GetType());
	}

	#region IYamlConvertible
	void IYamlConvertible.Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer)
	{
		emitter.Emit(new MappingStart(null, null, true, MappingStyle.Block));

		emitter.Emit(new Scalar(nameof(Path))); 
		emitter.Emit(new Scalar(Path));

		emitter.Emit(new Scalar(nameof(ExpectedType)));
		nestedObjectSerializer(ExpectedType, typeof(NodeSimpleTypeID));

		// Alternatively, you could provide the Data's AssemblyQualifiedName via the tag using "new Scalar(string, string)" 
		// and remove the need for having a separate field like "ExpectedType", but for my purposes I needed it
		emitter.Emit(new Scalar(nameof(Data)));
		nestedObjectSerializer(Data, ExpectedType.Type);

		emitter.Emit(new MappingEnd());
	}

	void IYamlConvertible.Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer)
	{
		parser.Expect<MappingStart>(); // Custom object starts

		// Name of the "Path" property
		parser.Expect<Scalar>(); 
		// Value of the property. Using the value from the field-initializer if a serialized value is not found (null).
		// This behaves exactly as the Yaml's implicit deserialization
		Path = (string)nestedObjectDeserializer(typeof(string)) ?? Path; 

		parser.Expect<Scalar>();
		ExpectedType = (NodeSimpleTypeID)nestedObjectDeserializer(typeof(NodeSimpleTypeID)) ?? ExpectedType;

		// If you don't want to have a field "ExpectedType", use the Tag property of the Scalar returned by this. See the Write(IEmitter, ObjectSerializer) method above.
		parser.Expect<Scalar>();
		if (ExpectedType.Type == null)
		{
			// The field holding the type of the Data property is empty/unset => Skip the section in the file associated with the Data property
			parser.SkipThisAndNestedEvents();
			Data = null;
		}
		else
		{
			int dataMappingStartColumn = parser.Peek<NodeEvent>().Start.Column;
			try
			{
				Data = nestedObjectDeserializer(ExpectedType.Type);
			}
			catch (Exception e)
			{
				Data = null;
				
				// On error deserializing the Data property, skip the section. This is done by skipping all events up until the MappingEnd event 
				// corresponding to its MappingStart event, and then skipping the MappingEnd itself. It should be after the last event on the 
				// same column, assuming the file is properly formatted
				while (parser.Current.Start.Column >= dataMappingStartColumn)
					parser.SkipThisAndNestedEvents();

				// Section for the Data property ends
				parser.Expect<MappingEnd>();
			}
		}
		parser.Expect<MappingEnd>(); // Custom object ends
	}
	#endregion
}

And here's the NodeSimpleTypeID class (this can be implemented more optimally, but it's outside this post's scope).

[Serializable]
public class NodeSimpleTypeID
{
	public string FullName {get; set;} = "";
	public string AssemblyFullName {get; set;} = "";

	[YamlIgnore]
	public string AssemblyQualifiedName => $"{FullName}, {AssemblyFullName}";

	[YamlIgnore]
	public Type Type 
	{ 
		get
		{
			if (!string.IsNullOrEmpty(FullName))
			{
				if (string.IsNullOrEmpty(AssemblyFullName))
					return Type.GetType(FullName);
				return Type.GetType(AssemblyQualifiedName);
			}
			return null;
		} 
	}


	public NodeSimpleTypeID(Type type)
	{
		FullName = type.FullName;
		AssemblyFullName = type.Assembly.FullName;
	}

	public NodeSimpleTypeID() { }
}

Hope this will be helpful.

I also suggest putting a Donate widget on the repo's page so people like me could show their love. 😊

Lucian

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant