diff --git a/lang/csharp/src/apache/codegen/AvroGen.cs b/lang/csharp/src/apache/codegen/AvroGen.cs index 349911754e9..c13053c25e1 100644 --- a/lang/csharp/src/apache/codegen/AvroGen.cs +++ b/lang/csharp/src/apache/codegen/AvroGen.cs @@ -1,4 +1,4 @@ -/** +/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -15,15 +15,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + using System; using System.Collections.Generic; -using System.Text; +using System.Linq; +using System.Text.RegularExpressions; namespace Avro { - class AvroGen + public static class AvroGen { - static int Main(string[] args) + private static readonly CommandLineArgument _createNamespaceDirectoriesParameter; + private static readonly CommandLineArgument _helpParameter; + private static readonly CommandLineArgument _namespaceParameter; + private static readonly CommandLineArgument _protocolParameter; + private static readonly CommandLineArgument _schemaParameter; + + static AvroGen() + { + // Primary usage parameters + _schemaParameter = new CommandLineArgument + { + Parameter = "-s", + FriendlyName = "Schema", + Usage = "avrogen -s [--namespace ]", + IsOptional = false, + }; + + _protocolParameter = new CommandLineArgument + { + Parameter = "-p", + FriendlyName = "Protocol", + Usage = "avrogen -p [--namespace ]", + IsOptional = false, + }; + + // Optional parameters + _helpParameter = new CommandLineArgument + { + Parameter = "-h", + Aliases = new List() { "--help" }, + FriendlyName = "Help", + Usage = "Show this screen.", + IsOptional = true, + }; + + _namespaceParameter = new CommandLineArgument + { + Parameter = "--namespace", + FriendlyName = "Namespace", + Usage = "Map an Avro schema/protocol namespace to a C# namespace. " + + "The format is \"my.avro.namespace:my.csharp.namespace\". " + + "May be specified multiple times to map multiple namespaces. ", + IsOptional = true, + }; + + _createNamespaceDirectoriesParameter = new CommandLineArgument + { + Parameter = "--create-namespace-directories", + FriendlyName = "Create Namespace Directories", + Usage = "Optional parameter to change how the files are output to the outputdir. " + + "Default is set to true. False will output the files to the root of the outputdir", + IsOptional = true, + }; + } + + private static int Main(string[] args) { // Print usage if no arguments provided if (args.Length == 0) @@ -33,164 +90,210 @@ static int Main(string[] args) } // Print usage if help requested - if (args[0] == "-h" || args[0] == "--help") + if (args.Contains("-h") || args.Contains("--help")) { Usage(); return 0; } - // Parse command line arguments - bool? isProtocol = null; - string inputFile = null; - string outputDir = null; - var namespaceMapping = new Dictionary(); - for (int i = 0; i < args.Length; ++i) + try { - if (args[i] == "-p") - { - if (i + 1 >= args.Length) - { - Console.Error.WriteLine("Missing path to protocol file"); - Usage(); - return 1; - } - - isProtocol = true; - inputFile = args[++i]; - } - else if (args[i] == "-s") + if (args.Contains(_protocolParameter.Parameter)) { - if (i + 1 >= args.Length) - { - Console.Error.WriteLine("Missing path to schema file"); - Usage(); - return 1; - } - - isProtocol = false; - inputFile = args[++i]; + ProtocolArguments protocolArguments = ParseProtocolInput(args); + Generator.GenerateProtocol(protocolArguments); } - else if (args[i] == "--namespace") + + if (args.Contains(_schemaParameter.Parameter)) { - if (i + 1 >= args.Length) - { - Console.Error.WriteLine("Missing namespace mapping"); - Usage(); - return 1; - } - - var parts = args[++i].Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length != 2) - { - Console.Error.WriteLine("Malformed namespace mapping. Required format is \"avro.namespace:csharp.namespace\""); - Usage(); - return 1; - } - - namespaceMapping[parts[0]] = parts[1]; + SchemaArguments schemaArguments = ParseSchemaInput(args); + Generator.GenerateSchema(schemaArguments); } - else if (outputDir == null) + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + Usage(); + return 1; + } + + return 0; + } + + private static void Usage() + { + Console.WriteLine( + $"{AppDomain.CurrentDomain.FriendlyName}\n\n" + + "Usage:\n" + + $" {_protocolParameter.Usage}\n" + + $" {_schemaParameter.Usage}\n\n" + + "Options:\n" + + FormatUsage(_helpParameter) + + FormatUsage(_namespaceParameter) + + FormatUsage(_createNamespaceDirectoriesParameter) + ); + return; + } + + private static string FormatUsage(CommandLineArgument parameter) + { + // Use the longest parameter. Add 2 spaces at front and 1 space at the end. + int length = _createNamespaceDirectoriesParameter.Parameter.Length + 3; + + string paramWithAliases = parameter.Parameter + " " + string.Join(' ', parameter.Aliases); + + string paramText = $" {paramWithAliases}{new string(' ', length - paramWithAliases.Length - 2)}"; + + List sentences = WordWrap(parameter.Usage, 60).ToList(); + + string fullText = paramText; + + for (int i = 0; i < sentences.Count; i++) + { + // spacing after parameter + if (i == 0) { - outputDir = args[i]; + fullText += sentences[i] + System.Environment.NewLine; + continue; } - else + + // Add sentence on new line + fullText += new string(' ', paramText.Length) + sentences[i] + System.Environment.NewLine; + } + + return fullText + System.Environment.NewLine; + } + + private static (string, string) GetThreePartArgument(string[] args, string option) + { + IEnumerable arguments = args.SkipWhile(i => i != option).Skip(1).Take(2); + + return (arguments.First(), arguments.Last()); + } + + private static string GetTwoPartArgument(string[] args, string option) + => args.SkipWhile(i => i != option).Skip(1).Take(1).FirstOrDefault(); + + private static List GetTwoPartArguments(string[] args, string option) + { + List arguments = new List(); + for (int i = 0; i < args.Length; i++) + { + if (args[i] == option) { - Console.Error.WriteLine("Unexpected command line argument: {0}", args[i]); - Usage(); + arguments.Add(args[i + 1]); + i++; } } - // Ensure we got all the command line arguments we need - bool isValid = true; - int rc = 0; - if (!isProtocol.HasValue || inputFile == null) + return arguments; + } + + private static bool ParseCreateNamespaceDirectories(string[] args) + { + string createDirectoriesValue = GetTwoPartArgument(args, _createNamespaceDirectoriesParameter.Parameter); + + if (string.IsNullOrWhiteSpace(createDirectoriesValue) | + (!createDirectoriesValue.Equals("true", StringComparison.InvariantCultureIgnoreCase) & + !createDirectoriesValue.Equals("false", StringComparison.InvariantCultureIgnoreCase))) { - Console.Error.WriteLine("Must provide either '-p ' or '-s '"); - isValid = false; + throw new AvroException($"{_createNamespaceDirectoriesParameter} parameters must have a value of true or false"); } - else if (outputDir == null) + + return createDirectoriesValue.Equals("true", StringComparison.InvariantCultureIgnoreCase); + } + + private static Dictionary ParseNamespace(string[] args) + { + List namespaces = GetTwoPartArguments(args, _namespaceParameter.Parameter); + + if (namespaces.Count == 0) { - Console.Error.WriteLine("Must provide 'outputdir'"); - isValid = false; + throw new AvroException("Missing namespace mapping"); } + Dictionary arguments = new Dictionary(); - if (!isValid) + foreach (string ns in namespaces) { - Usage(); - rc = 1; + string[] parts = ns.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + { + Console.Error.WriteLine($"Malformed namespace mapping. Required format is \"avro.namespace:csharp.namespace\" Actual: {ns}"); + } + + arguments[parts[0]] = parts[1]; } - else if (isProtocol.Value) - rc = GenProtocol(inputFile, outputDir, namespaceMapping); - else - rc = GenSchema(inputFile, outputDir, namespaceMapping); - return rc; + return arguments; } - static void Usage() + private static ProtocolArguments ParseProtocolInput(string[] args) { - Console.WriteLine("{0}\n\n" + - "Usage:\n" + - " avrogen -p [--namespace ]\n" + - " avrogen -s [--namespace ]\n\n" + - "Options:\n" + - " -h --help Show this screen.\n" + - " --namespace Map an Avro schema/protocol namespace to a C# namespace.\n" + - " The format is \"my.avro.namespace:my.csharp.namespace\".\n" + - " May be specified multiple times to map multiple namespaces.\n", - AppDomain.CurrentDomain.FriendlyName); - return; + ProtocolArguments arguments = new ProtocolArguments(); + + (string, string) argument = GetThreePartArgument(args, _protocolParameter.Parameter); + + arguments.ProtocolFile = argument.Item1; + arguments.OutputDirectory = argument.Item2; + + if (args.Contains(_namespaceParameter.Parameter)) + { + arguments.NamespaceMapping = ParseNamespace(args); + } + + return arguments; } - static int GenProtocol(string infile, string outdir, - IEnumerable> namespaceMapping) + + private static SchemaArguments ParseSchemaInput(string[] args) { - try - { - string text = System.IO.File.ReadAllText(infile); - Protocol protocol = Protocol.Parse(text); + SchemaArguments arguments = new SchemaArguments(); - CodeGen codegen = new CodeGen(); - codegen.AddProtocol(protocol); + (string, string) argument = GetThreePartArgument(args, _schemaParameter.Parameter); - foreach (var entry in namespaceMapping) - codegen.NamespaceMapping[entry.Key] = entry.Value; + arguments.SchemaFile = argument.Item1; + arguments.OutputDirectory = argument.Item2; - codegen.GenerateCode(); - codegen.WriteTypes(outdir); + if (args.Contains(_namespaceParameter.Parameter)) + { + arguments.NamespaceMapping = ParseNamespace(args); } - catch (Exception ex) + + if (args.Contains(_createNamespaceDirectoriesParameter.Parameter)) { - Console.Error.WriteLine("Exception occurred. " + ex.Message); - return 1; + arguments.CreateNamespaceDirectories = ParseCreateNamespaceDirectories(args); } - return 0; + return arguments; } - static int GenSchema(string infile, string outdir, - IEnumerable> namespaceMapping) + + private static IEnumerable WordWrap(string text, int width) { - try - { - string text = System.IO.File.ReadAllText(infile); - Schema schema = Schema.Parse(text); + string forcedBreakZonePattern = @"\n"; + string normalBreakZonePattern = @"\s+|(?<=[-,.;])|$"; - CodeGen codegen = new CodeGen(); - codegen.AddSchema(schema); + var forcedZones = Regex.Matches(text, forcedBreakZonePattern).Cast().ToList(); + var normalZones = Regex.Matches(text, normalBreakZonePattern).Cast().ToList(); - foreach (var entry in namespaceMapping) - codegen.NamespaceMapping[entry.Key] = entry.Value; + int start = 0; - codegen.GenerateCode(); - codegen.WriteTypes(outdir); - } - catch (Exception ex) + while (start < text.Length) { - Console.Error.WriteLine("Exception occurred. " + ex.Message); - return 1; - } + var zone = + forcedZones.Find(z => z.Index >= start && z.Index <= start + width) ?? + normalZones.FindLast(z => z.Index >= start && z.Index <= start + width); - return 0; + if (zone == null) + { + yield return text.Substring(start, width); + start += width; + } + else + { + yield return text.Substring(start, zone.Index - start); + start = zone.Index + zone.Length; + } + } } } } diff --git a/lang/csharp/src/apache/codegen/Generator.cs b/lang/csharp/src/apache/codegen/Generator.cs new file mode 100644 index 00000000000..81234939b49 --- /dev/null +++ b/lang/csharp/src/apache/codegen/Generator.cs @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; + +namespace Avro +{ + public static class Generator + { + /// + /// Generates the protocol. + /// + /// The protocol arguments. + public static void GenerateProtocol(ProtocolArguments protocolArguments) + { + ValidateArguments(protocolArguments); + string text = GetFileContent(protocolArguments.ProtocolFile); + Protocol protocol = Protocol.Parse(text); + CodeGen codeGen = new CodeGen(); + codeGen.AddProtocol(protocol); + AddNamespaceMapping(protocolArguments.NamespaceMapping, ref codeGen); + codeGen.GenerateCode(); + codeGen.WriteTypes(protocolArguments.OutputDirectory); + } + + /// + /// Generates the schema. + /// + /// The schema arguments. + public static void GenerateSchema(SchemaArguments schemaArguments) + { + ValidateArguments(schemaArguments); + string text = GetFileContent(schemaArguments.SchemaFile); + Schema schema = Schema.Parse(text); + CodeGen codeGen = new CodeGen(); + codeGen.AddSchema(schema); + AddNamespaceMapping(schemaArguments.NamespaceMapping, ref codeGen); + codeGen.GenerateCode(); + codeGen.WriteTypes(schemaArguments.OutputDirectory, schemaArguments.CreateNamespaceDirectories); + } + + /// + /// Adds the namespace mapping. + /// + /// The namespaces. + /// The code gen instance. + /// codeGen + private static void AddNamespaceMapping(Dictionary namespaces, ref CodeGen codeGen) + { + if (codeGen == null) + { + throw new ArgumentNullException(nameof(codeGen)); + } + + foreach (KeyValuePair entry in namespaces) + { + codeGen.NamespaceMapping[entry.Key] = entry.Value; + } + } + + /// + /// Gets the content of the file. + /// + /// The file path. + /// + /// Content is empty for {filePath} + private static string GetFileContent(string filePath) + { + string text = System.IO.File.ReadAllText(filePath); + + if (string.IsNullOrWhiteSpace(text)) + { + throw new AvroException($"Content is empty for {filePath}"); + } + + return text; + } + + /// + /// Validates the arguments. + /// + /// The schema arguments. + /// schemaArguments + private static void ValidateArguments(SchemaArguments schemaArguments) + { + if (schemaArguments == null) + { + throw new ArgumentNullException(nameof(schemaArguments)); + } + + ValidateFilePath(schemaArguments.SchemaFile); + ValidateDirectory(schemaArguments.OutputDirectory); + } + + /// + /// Validates the arguments. + /// + /// The protocol arguments. + /// protocolArguments + private static void ValidateArguments(ProtocolArguments protocolArguments) + { + if (protocolArguments == null) + { + throw new ArgumentNullException(nameof(protocolArguments)); + } + + ValidateFilePath(protocolArguments.ProtocolFile); + ValidateDirectory(protocolArguments.OutputDirectory); + } + + /// + /// Validates the directory. + /// + /// The directory. + /// Directory can not be found {directory} + private static void ValidateDirectory(string directory) + { + if (!System.IO.Directory.Exists(directory)) + { + throw new System.IO.DirectoryNotFoundException($"Directory can not be found {directory}"); + } + } + + /// + /// Validates the file path. + /// + /// The file path. + /// Path to input was not found {filePath} + private static void ValidateFilePath(string filePath) + { + if (!System.IO.File.Exists(filePath)) + { + throw new System.IO.FileNotFoundException($"Path to input was not found {filePath}"); + } + } + } +} diff --git a/lang/csharp/src/apache/codegen/Models/Arguments.cs b/lang/csharp/src/apache/codegen/Models/Arguments.cs new file mode 100644 index 00000000000..318e2e9fcf3 --- /dev/null +++ b/lang/csharp/src/apache/codegen/Models/Arguments.cs @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; + +namespace Avro +{ + public abstract class Arguments + { + /// + /// Initializes a new instance of the class. + /// + public Arguments() + { + NamespaceMapping = new Dictionary(); + } + + /// + /// Gets or sets the namespace mapping. + /// + /// + /// The namespace mapping. + /// + public Dictionary NamespaceMapping { get; set; } + + /// + /// Gets or sets the output directory. + /// + /// + /// The output directory. + /// + public string OutputDirectory { get; set; } + + /// + /// Gets a value indicating whether this instance has namespace mapping. + /// + /// + /// true if this instance has namespace mapping; otherwise, false. + /// + internal bool HasNamespaceMapping => NamespaceMapping.Count > 0; + + /// + /// Gets a value indicating whether this instance has output directory. + /// + /// + /// true if this instance has output directory; otherwise, false. + /// + internal bool HasOutputDirectory => !string.IsNullOrEmpty(OutputDirectory); + } +} diff --git a/lang/csharp/src/apache/codegen/Models/CommandLineArgument.cs b/lang/csharp/src/apache/codegen/Models/CommandLineArgument.cs new file mode 100644 index 00000000000..9e553c43c29 --- /dev/null +++ b/lang/csharp/src/apache/codegen/Models/CommandLineArgument.cs @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; + +namespace Avro +{ + internal class CommandLineArgument + { + /// + /// Initializes a new instance of the class. + /// + public CommandLineArgument() + { + Aliases = new List(); + } + + /// + /// Gets or sets the aliases, which are variations allowed instead of the parameter. + /// + /// + /// The aliases. + /// + public List Aliases { get; set; } + + /// + /// Gets or sets the Friendly Name. + /// + /// + /// The Friendly Name. + /// + public string FriendlyName { get; set; } + + /// + /// Gets or sets a value indicating whether this parameter is optional. + /// + /// + /// true if this parameter is optional; otherwise, false. + /// + public bool IsOptional { get; set; } + + /// + /// Gets or sets the parameter. + /// + /// + /// The parameter. + /// + public string Parameter { get; set; } + + /// + /// Gets or sets the usage text. + /// + /// + /// The usage text. + /// + public string Usage { get; set; } + } +} diff --git a/lang/csharp/src/apache/codegen/Models/ProtocolArguments.cs b/lang/csharp/src/apache/codegen/Models/ProtocolArguments.cs new file mode 100644 index 00000000000..5b26b6ba221 --- /dev/null +++ b/lang/csharp/src/apache/codegen/Models/ProtocolArguments.cs @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Avro +{ + public class ProtocolArguments : Arguments + { + /// + /// Initializes a new instance of the class. + /// + public ProtocolArguments() : base() + { + } + + /// + /// Gets or sets the protocol file. + /// + /// + /// The protocol file. + /// + public string ProtocolFile { get; set; } + + /// + /// Gets a value indicating whether this instance has protocol file. + /// + /// + /// true if this instance has protocol file; otherwise, false. + /// + internal bool HasProtocolFile => !string.IsNullOrEmpty(ProtocolFile); + } +} diff --git a/lang/csharp/src/apache/codegen/Models/SchemaArguments.cs b/lang/csharp/src/apache/codegen/Models/SchemaArguments.cs new file mode 100644 index 00000000000..15bd3a8abb1 --- /dev/null +++ b/lang/csharp/src/apache/codegen/Models/SchemaArguments.cs @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Avro +{ + public class SchemaArguments : Arguments + { + /// + /// Initializes a new instance of the class. + /// + public SchemaArguments() : base() + { + CreateNamespaceDirectories = true; + } + + /// + /// Gets or sets a value indicating whether [create namespace directories]. + /// + /// + /// true if [create namespace directories]; otherwise, false. + /// + public bool CreateNamespaceDirectories { get; set; } + + /// + /// Gets or sets the schema file. + /// + /// + /// The schema file. + /// + public string SchemaFile { get; set; } + + /// + /// Gets a value indicating whether this instance has schema file. + /// + /// + /// true if this instance has schema file; otherwise, false. + /// + internal bool HasSchemaFile => !string.IsNullOrEmpty(SchemaFile); + } +} diff --git a/lang/csharp/src/apache/main/CodeGen/CodeGen.cs b/lang/csharp/src/apache/main/CodeGen/CodeGen.cs index 922bfe02fda..d88c60c6116 100644 --- a/lang/csharp/src/apache/main/CodeGen/CodeGen.cs +++ b/lang/csharp/src/apache/main/CodeGen/CodeGen.cs @@ -1110,7 +1110,8 @@ public virtual void WriteCompileUnit(string outputFile) /// Writes each types in each namespaces into individual files. /// /// name of directory to write to. - public virtual void WriteTypes(string outputdir) + /// if set to true [create namespace directories]. + public virtual void WriteTypes(string outputdir, bool createNamespaceDirectories = true) { var cscp = new CSharpCodeProvider(); @@ -1125,9 +1126,12 @@ public virtual void WriteTypes(string outputdir) var ns = nsc[i]; string dir = outputdir; - foreach (string name in CodeGenUtil.Instance.UnMangle(ns.Name).Split('.')) + if (createNamespaceDirectories) { - dir = Path.Combine(dir, name); + foreach (string name in CodeGenUtil.Instance.UnMangle(ns.Name).Split('.')) + { + dir = Path.Combine(dir, name); + } } Directory.CreateDirectory(dir);