Created
October 27, 2017 12:57
-
-
Save thefringeninja/6df7fa16518a5c98d328331afb1ce0c0 to your computer and use it in GitHub Desktop.
Generate Message Contracts via CSX Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! "netcoreapp1.1" | |
#r "nuget:NetStandard.Library,1.6.1" | |
#r "nuget:Dotnet.Script.Core,0.13.0-beta" | |
#r "nuget:SharpYaml,1.6.1" | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Xml; | |
using Dotnet.Script.Core; | |
using SharpYaml.Serialization; | |
var writer = new MessageContractWriter(new DirectoryInfo(Directory.GetCurrentDirectory())); | |
await writer.GenerateMessageContracts("YourNamespace"); | |
class MessageContractWriter { | |
private readonly DirectoryInfo _directory; | |
private readonly FileInfo _output; | |
public MessageContractWriter(DirectoryInfo directory) | |
{ | |
_directory = directory ?? throw new ArgumentNullException(nameof(directory)); | |
if (!directory.Exists) { | |
throw new ArgumentException(nameof(directory)); | |
} | |
_output = new FileInfo(Path.Combine(_directory.FullName, "Contracts.cs")); | |
} | |
public async Task GenerateMessageContracts(string ns) { | |
var model = ComposedModel.ParsePath(ns, _directory.FullName); | |
using (var writer = _output.CreateText()) { | |
await writer.WriteLineAsync("// -----------------------------------------------------------------------------------------------------------------"); | |
await writer.WriteLineAsync("// Auto-generated code. Please do not change manually. Change the yaml file instead and rerun the text transformation."); | |
await writer.WriteLineAsync("// -------------------------------------------------------------------------------------------------------------------"); | |
await writer.WriteLineAsync($"namespace {@ns}.Messages"); | |
await writer.WriteLineAsync("{"); | |
await writer.WriteLineAsync(" using System;"); | |
await writer.WriteLineAsync(" using System.Runtime.Serialization;"); | |
foreach (var namedVersionedContracts in model.NamedVersionedContracts) { | |
await writer.WriteLineAsync($" public static partial class {namedVersionedContracts.Key}"); | |
await writer.WriteLineAsync(" {"); | |
foreach (var versionedContractLookup in namedVersionedContracts) { | |
foreach (var versionedContract in versionedContractLookup) { | |
var grouping = namedVersionedContracts.Key.EndsWith("Messages") | |
? namedVersionedContracts.Key.Substring(0, namedVersionedContracts.Key.Length - "Messages".Length) | |
: namedVersionedContracts.Key; | |
var @namespace = ns + "." + grouping + ".V" + versionedContract.Key; | |
await writer.WriteLineAsync($" public static partial class V{versionedContract.Key}"); | |
await writer.WriteLineAsync(" {"); | |
foreach (var contract in versionedContract) { | |
await writer.WriteLineAsync($@" [DataContract (Namespace = ""{@namespace}"")]"); | |
await writer.WriteLineAsync($@" public partial class {contract.Name} {contract.InheritsFrom}"); | |
await writer.WriteLineAsync(" {"); | |
if (contract.Properties.Any(p => p.IsArray)) { | |
await writer.WriteLineAsync($" public {contract.Name}()"); | |
await writer.WriteLineAsync(" {"); | |
foreach (var property in contract.Properties.Where(p => p.IsArray)) { | |
await writer.WriteLineAsync($" {property.Name} = new {property.ElementDataType}[0];"); | |
} | |
await writer.WriteLineAsync(" }"); | |
} | |
var count = 0; | |
foreach (var property in contract.Properties.Where(p => !p.Name.StartsWith("_"))) { | |
await writer.WriteLineAsync($" [DataMember(Order = {count})] public {property.DataType} {property.Name} {{ get; set; }}"); | |
count++; | |
} | |
if (contract._ToString != null) { | |
await writer.WriteLineAsync($@" public override string ToString() => $""{contract._ToString}"";"); | |
} | |
await writer.WriteLineAsync(" }"); | |
} | |
await writer.WriteLineAsync(" }"); | |
} | |
} | |
await writer.WriteLineAsync(" }"); | |
} | |
await writer.WriteLineAsync("}"); | |
} | |
} | |
} | |
public abstract class NamedPropertyContainer | |
{ | |
private string _name; | |
protected NamedPropertyContainer() | |
{ | |
_name = "<not_specified>"; | |
Properties = new List<Property>(); | |
} | |
public string Name | |
{ | |
get { return _name; } | |
set | |
{ | |
if(string.IsNullOrEmpty(value)) | |
throw new ArgumentNullException("value"); | |
_name = value; | |
} | |
} | |
public List<Property> Properties { get; private set; } | |
public int Version { get; set; } | |
virtual public string InheritsFrom => string.Empty; | |
public string _ToString { get { return Properties.Where(p => p.Name == "_ToString").Select(p => p.DataType).FirstOrDefault(); } } | |
} | |
public class Property | |
{ | |
public Property() | |
{ | |
_name = "<not_specified>"; | |
_dataType = "<not_specified>"; | |
} | |
private string _name; | |
private string _dataType; | |
public string Name | |
{ | |
get { return _name; } | |
set | |
{ | |
if (string.IsNullOrEmpty(value)) | |
throw new ArgumentNullException("value"); | |
_name = value; | |
} | |
} | |
public string DataType | |
{ | |
get { return _dataType; } | |
set | |
{ | |
if (string.IsNullOrEmpty(value)) | |
throw new ArgumentNullException("value"); | |
_dataType = value; | |
} | |
} | |
public bool IsArray => DataType.EndsWith("[]"); | |
public string ElementDataType => IsArray ? DataType.Substring(0, DataType.Length - 2) : DataType; | |
} | |
public abstract class Message : NamedPropertyContainer | |
{ | |
} | |
public class Command : Message | |
{ | |
override public string InheritsFrom => ": ICommand"; | |
} | |
public class Event : Message | |
{ | |
override public string InheritsFrom => ": IEvent"; | |
} | |
public class DataType : NamedPropertyContainer | |
{ | |
} | |
public class ComposedModel | |
{ | |
public string Namespace { get; } | |
public List<NamedModel> NamedModels | |
{ | |
get; set; | |
} | |
public ILookup<string, ILookup<int, NamedPropertyContainer>> NamedVersionedContracts | |
=> (from model in NamedModels | |
select new | |
{ | |
model.Name, | |
contracts = model.Contracts.ToLookup(x => x.Version) | |
}).ToLookup(x => x.Name, x => x.contracts); | |
public ComposedModel(string @namespace) | |
{ | |
Namespace = @namespace; | |
NamedModels = new List<NamedModel>(); | |
} | |
public static ComposedModel ParsePath(string @namespace, string path) | |
{ | |
if (path == null) throw new ArgumentNullException("path"); | |
var model = new ComposedModel(@namespace); | |
foreach(var file in Directory.EnumerateFiles(path, "*.yaml", SearchOption.AllDirectories)) | |
{ | |
if (file.EndsWith("Generated.yaml")) continue; | |
model.NamedModels.Add(NamedModel.ParseFile(file, model.Namespace)); | |
} | |
return model; | |
} | |
} | |
public class NamedModel | |
{ | |
private readonly string _namespace; | |
public NamedModel(string name, string @namespace) | |
{ | |
_namespace = @namespace; | |
Name = name; | |
Contracts = new List<NamedPropertyContainer>(); | |
} | |
public string Name { get; } | |
public List<NamedPropertyContainer> Contracts { get; private set; } | |
public static NamedModel ParseFile(string path, string @namespace) | |
{ | |
if (path == null) throw new ArgumentNullException("path"); | |
var name = Path.GetFileNameWithoutExtension(path); | |
using (var yamlSource = File.OpenText(path)) | |
{ | |
var stream = new YamlStream(); | |
stream.Load(yamlSource); | |
var visitor = new NamedModelVisitor(name, @namespace); | |
stream.Accept(visitor); | |
return visitor.Model; | |
} | |
} | |
} | |
public class NamedModelVisitor : YamlVisitor | |
{ | |
private readonly NamedModel _model; | |
private int _mappingIndentation; | |
private int _scalarCount; | |
private NamedPropertyContainer Current => _model.Contracts[_model.Contracts.Count - 1]; | |
private int _version; | |
public NamedModelVisitor(string name, string @namespace) | |
{ | |
_model = new NamedModel(name, @namespace); | |
} | |
public NamedModel Model | |
{ | |
get { return _model; } | |
} | |
protected override void Visit(YamlDocument document) | |
{ | |
_mappingIndentation = 0; | |
_scalarCount = 0; | |
} | |
protected override void Visit(YamlScalarNode scalar) | |
{ | |
switch (_mappingIndentation) | |
{ | |
case 1: | |
_version = Int32.Parse(scalar.Value.Remove(0, 1)); | |
break; | |
case 2: | |
if (scalar.Value.EndsWith("?")) // command | |
{ | |
_model.Contracts.Add(new Command { Name = scalar.Value.Substring(0, scalar.Value.Length - 1), Version = _version }); | |
} | |
else if (scalar.Value.EndsWith("!")) // event | |
{ | |
_model.Contracts.Add(new Event { Name = scalar.Value.Substring(0, scalar.Value.Length - 1), Version = _version }); | |
} | |
else // some shared thing | |
{ | |
_model.Contracts.Add(new DataType { Name = scalar.Value, Version = _version }); | |
} | |
break; | |
case 3: | |
switch ((_scalarCount % 2)) | |
{ | |
case 0: | |
Current.Properties.Add(new Property { Name = scalar.Value }); | |
break; | |
case 1: | |
Current.Properties[Current.Properties.Count - 1].DataType = scalar.Value; | |
break; | |
} | |
_scalarCount++; | |
break; | |
} | |
} | |
protected override void Visit(YamlMappingNode mapping) | |
{ | |
_mappingIndentation += 1; | |
_scalarCount = 0; | |
} | |
protected override void Visited(YamlMappingNode mapping) | |
{ | |
_mappingIndentation -= 1; | |
_scalarCount = 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment