[PATCH] Port [Db|Sql]Metal to Mono.Options

38 views
Skip to first unread message

Emanuele Aina

unread,
Nov 4, 2009, 4:19:45 PM11/4/09
to DbLinq
I've replaced the custom (broken) command line option parsing system
with Mono.Options.

At least I can now use unix-style absolute paths with DbMetal. <g>

Here is a summary of the patch and the relevant changes to Parameters.cs
while the full patch is attached.

---
src/DbMetal/AbstractParameters.cs | 695 -------------
src/DbMetal/DbMetal.csproj | 2 +-
src/DbMetal/Generator/Implementation/Processor.cs | 23 +-
src/DbMetal/Mono/Options.cs | 1112 +++++++++++++++++++++
src/DbMetal/Parameters.cs | 199 +++-
src/DbMetal/SqlMetal.csproj | 2 +-
6 files changed, 1281 insertions(+), 752 deletions(-)
delete mode 100644 src/DbMetal/AbstractParameters.cs
create mode 100644 src/DbMetal/Mono/Options.cs

diff --git a/src/DbMetal/Parameters.cs b/src/DbMetal/Parameters.cs
index a17514d..4340f0f 100644
--- a/src/DbMetal/Parameters.cs
+++ b/src/DbMetal/Parameters.cs
@@ -28,38 +28,39 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
+using System.IO;
+using System.Reflection;
+using DbLinq.Util;
+
+using Mono.Options;

namespace DbMetal
{
[DebuggerDisplay("Parameters from {Provider}, server={Server}")]
- public class Parameters : AbstractParameters
+ public class Parameters
{
/// <summary>
/// user name for database access
/// SQLMetal compatible
/// </summary>
- [Option("Login user ID.", ValueName = "name", Group = 1)]
public string User { get; set; }

/// <summary>
/// user password for database access
/// SQLMetal compatible
/// </summary>
- [Option("Login password.", ValueName = "password", Group = 1)]
public string Password { get; set; }

/// <summary>
/// server host name
/// SQLMetal compatible
/// </summary>
- [Option("Database server name.", ValueName = "name", Group = 1)]
public string Server { get; set; }

/// <summary>
/// database name
/// SQLMetal compatible
/// </summary>
- [Option("Database catalog on server.", ValueName = "name", Group = 1)]
public string Database { get; set; }

/// <summary>
@@ -67,60 +68,49 @@ namespace DbMetal
/// Database is always used to generate the specific DataContext name
/// SQLMetal compatible
/// </summary>
- [Option("Database connection string. Cannot be used with /server, /user or /password options.",
- ValueName = "connection string", Group = 1)]
public string Conn { get; set; }

/// <summary>
/// the namespace to put our classes into
/// SQLMetal compatible
/// </summary>
- [Option("Namespace of generated code (default: no namespace).", ValueName = "name", Group = 4)]
public string Namespace { get; set; }

/// <summary>
/// the language to generate classes for
/// SQLMetal compatible
/// </summary>
- [Option("Language for source code: C#, C#2 or VB (default: derived from extension on code file name).", ValueName = "name", Group = 4)]
public string Language { get; set; }

/// <summary>
/// If present, write out C# code
/// SQLMetal compatible
/// </summary>
- [Option("Output as source code. Cannot be used with /dbml option.", ValueName = "file", Group = 3)]
public string Code { get; set; }

/// <summary>
/// If present, write out DBML XML representing the DB
/// SQLMetal compatible
/// </summary>
- [Option("Output as dbml. Cannot be used with /map option.", ValueName = "file", Group = 3)]
public string Dbml { get; set; }

/// <summary>
/// when true, we will call Singularize()/Pluralize() functions.
/// SQLMetal compatible
/// </summary>
- [Option("Automatically pluralize or singularize class and member names using specified culture rules.", Group = 4)]
public bool Pluralize { get; set; }

- [Option("Specify culture for word recognition and pluralization (default=\"en\").", Group = 4)]
public string Culture { get; set; }

/// <summary>
/// Load object renamings from an xml file
/// DbLinq specific
/// </summary>
- [Option("Use mapping file.", ValueName = "file", Group = 3)]
- [Alternate("renamesFile")]
public string Aliases { get; set; }

/// <summary>
/// this is the "input file" parameter
/// </summary>
- [File("input file", "DBML input file.")]
public string SchemaXmlFile
{
get
@@ -129,38 +119,29 @@ namespace DbMetal
}
}

- [Option("Generate schema in code files (default='true').", Group = 4)]
public bool Schema { get; set; }

/// <summary>
/// base class from which all generated entities will inherit
/// SQLMetal compatible
/// </summary>
- [Option("Base class of entity classes in the generated code (default: entities have no base class).",
- ValueName = "type", Group = 4)]
public string EntityBase { get; set; }

/// <summary>
/// Interfaces to be implemented
/// </summary>
- [Option("Comma separated base interfaces of entity classes in the generated code (default: entities implement INotifyPropertyChanged).",
- ValueName = "interface(s)", Group = 4)]
public string EntityInterfaces { get; set; }
public string[] EntityImplementedInterfaces { get { return GetArray(EntityInterfaces); } }

/// <summary>
/// Extra attributes to be implemented by class
/// </summary>
- [Option("Comma separated attributes of entity classes in the generated code.",
- ValueName = "attribute(s)", Group = 4)]
public string EntityAttributes { get; set; }
public string[] EntityExposedAttributes { get { return GetArray(EntityAttributes); } }

/// <summary>
/// Extra attributes to be implemented by class
/// </summary>
- [Option("Comma separated attributes of entity members in the generated code.",
- ValueName = "attribute(s)", Group = 4)]
public string MemberAttributes { get; set; }
public string[] MemberExposedAttributes { get { return GetArray(MemberAttributes); } }

@@ -168,29 +149,20 @@ namespace DbMetal
/// base class from which all generated entities will inherit
/// SQLMetal compatible
/// </summary>
- [Option("Generates overrides for Equals() and GetHashCode() methods.", Group = 4)]
public bool GenerateEqualsAndHash { get; set; }

/// <summary>
/// export stored procedures
/// SQLMetal compatible
/// </summary>
- [Option("Extract stored procedures.", Group = 2)]
public bool Sprocs { get; set; }

/// <summary>
/// preserve case of database names
/// DbLinq specific
/// </summary>
- [Option("Transform names as indicated (default: net; may be: leave, pascal, camel, net).", Group = 4)]
public string Case { get; set; }

- /// <summary>
- /// ??
- /// DbLinq specific
- /// </summary>
- public bool VerboseForeignKeys { get; set; }
-
bool useDomainTypes = true;

/// <summary>
@@ -211,35 +183,42 @@ namespace DbMetal
/// picrap comment: you may use the tool to write output to Visual Studio output window instead of a console window
/// DbLinq specific
/// </summary>
- [Option("Wait for a key to be pressed after processing.", Group = 4)]
public bool ReadLineAtExit { get; set; }

/// <summary>
/// specifies a provider (which here is a pair or ISchemaLoader and IDbConnection implementors)
/// SQLMetal compatible
/// </summary>
- [Option("Specify provider. May be Ingres, MySql, Oracle, OracleODP, PostgreSql or Sqlite.",
- ValueName = "provider", Group = 1)]
public string Provider { get; set; }

/// <summary>
/// For fine tuning, we allow to specifiy an ISchemaLoader
/// DbLinq specific
/// </summary>
- [Option("Specify a custom ISchemaLoader implementation type.", ValueName = "type", Group = 1)]
public string DbLinqSchemaLoaderProvider { get; set; }

/// <summary>
/// For fine tuning, we allow to specifiy an IDbConnection
/// DbLinq specific
/// </summary>
- [Option("Specify a custom IDbConnection implementation type.", ValueName = "type", Group = 1)]
public string DatabaseConnectionProvider { get; set; }

- [Alternate("generate-timestamps")]
- [Option("Generate timestampes within the /code:<file> file. True by default.")]
public bool GenerateTimestamps { get; set; }

+ public bool Help { get; set; }
+
+ public IList<string> Extra = new List<string>();
+
+ TextWriter log;
+ public TextWriter Log
+ {
+ get { return log ?? Console.Out; }
+ set { log = value; }
+ }
+
+
+ protected OptionSet Options;
+
public Parameters()
{
Schema = true;
@@ -248,14 +227,83 @@ namespace DbMetal
EntityInterfaces = "INotifyPropertyChanged";//INotifyPropertyChanging INotifyPropertyChanged IModified
}

- public IEnumerable<Parameters> GetBatch(IList<string> args)
+ /// <summary>
+ /// Converts a list separated by a comma to a string array
+ /// </summary>
+ /// <param name="list"></param>
+ /// <returns></returns>
+ public string[] GetArray(string list)
+ {
+ if (string.IsNullOrEmpty(list))
+ return new string[0];
+ return (from entityInterface in list.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ select entityInterface.Trim()).ToArray();
+ }
+
+ public void Parse(IList<string> args)
{
- return GetParameterBatch<Parameters>(args);
+ Options = new OptionSet() {
+ { "c|conn=", "Database {CONNECTION STRING}. Cannot be used with /server, /user or /password options.", conn => Conn = conn },
+ { "u|user=", "Login user {NAME}.", name => User = name },
+ { "p|password=", "Login {PASSWORD}.", password => Password = password },
+ { "s|server=", "Database server {NAME}.", name => Server = name },
+ { "d|database=", "Database catalog {NAME} on server.", name => Database = name },
+ { "provider=", "Specify {PROVIDER}. May be Ingres, MySql, Oracle, OracleODP, PostgreSql or Sqlite.", provider => Provider = provider },
+ { "dbLinqSchemaLoaderProvider=", "Specify a custom ISchemaLoader implementation {TYPE}.", type => DbLinqSchemaLoaderProvider = type },
+ { "databaseConnectionProvider=", "Specify a custom IDbConnection implementation {TYPE}.", type => DatabaseConnectionProvider = type },
+ { "code=", "Output as source code to {FILE}. Cannot be used with /dbml option.", file => Code = file },
+ { "dbml=", "Output as dbml to {FILE}. Cannot be used with /map option.", file => Dbml = file },
+ { "language=", "Language {NAME} for source code: C#, C#2 or VB (default: derived from extension on code file name).", name => Language = name },
+ { "aliases|renamesFile=", "Use mapping {FILE}.", file => Aliases = file },
+ { "schema", "Generate schema in code files (default='true').", (bool v) => Schema = v },
+ { "namespace=", "Namespace {NAME} of generated code (default: no namespace).", name => Namespace = name },
+ { "entityBase=", "Base {TYPE} of entity classes in the generated code (default: entities have no base class).", type => EntityBase = type },
+ { "entityInterfaces=", "Comma separated base {INTERFACE(S)} of entity classes in the generated code (default: entities implement INotifyPropertyChanged).", interfaces => EntityInterfaces = interfaces },
+ { "entityAttributes=", "Comma separated {ATTRIBUTE(S)} of entity classes in the generated code.", attributes => EntityAttributes = attributes },
+ { "memberAttributes=", "Comma separated {ATTRIBUTE(S)} of entity members in the generated code.", attributes => MemberAttributes = attributes },
+ { "generateEqualsAndHash", "Generates overrides for Equals() and GetHashCode() methods.", (bool v) => GenerateEqualsAndHash = v },
+ { "sprocs", "Extract stored procedures.", v => Sprocs = true },
+ { "pluralize", "Automatically pluralize or singularize class and member names using specified culture rules.", (bool v) => Pluralize = v },
+ { "culture=", "Specify {CULTURE} for word recognition and pluralization (default=\"en\").", culture => Culture = culture },
+ { "case=", "Transform names with the indicated {STYLE} (default: net; may be: leave, pascal, camel, net).", style => Case = style },
+ { "generate-timestamps|generateTimestamps", "Generate timestampes in the generated code. True by default.", (bool v) => GenerateTimestamps = v },
+ { "readlineAtExit", "Wait for a key to be pressed after processing.", (bool v) => ReadLineAtExit = v },
+ { "h|?|help", "Show this help", v => Help = true }
+ };
+
+ Extra = Options.Parse(args);
}

#region Help

- protected override void WriteHeaderContents()
+ public void WriteHelp()
+ {
+ WriteHeader(); // includes a WriteLine()
+ WriteSyntax();
+ WriteLine();
+ WriteSummary();
+ WriteLine();
+ Options.WriteOptionDescriptions(Log);
+ WriteLine();
+ WriteExamples();
+ }
+
+ bool headerWritten;
+
+ /// <summary>
+ /// Writes the application header
+ /// </summary>
+ public void WriteHeader()
+ {
+ if (!headerWritten)
+ {
+ WriteHeaderContents();
+ WriteLine();
+ headerWritten = true;
+ }
+ }
+
+ protected void WriteHeaderContents()
{
var version = ApplicationVersion;
Write("DbLinq Database mapping generator 2008 version {0}.{1}", version.Major, version.Minor);
@@ -263,7 +311,10 @@ namespace DbMetal
Write("Distributed under the MIT licence (http://linq.to/db/license)");
}

- public override void WriteSummary()
+ /// <summary>
+ /// Writes a small summary
+ /// </summary>
+ public void WriteSummary()
{
Write(" Generates code and mapping for DbLinq. SqlMetal can:");
Write(" - Generate source code and mapping attributes or a mapping file from a database.");
@@ -271,6 +322,64 @@ namespace DbMetal
Write(" - Generate code and mapping attributes or mapping file from a dbml file.");
}

+ public void WriteSyntax()
+ {
+ var syntax = new StringBuilder();
+ syntax.AppendFormat("{0} [OPTIONS] [<DBML INPUT FILE>]", ApplicationName);
+ Write(syntax.ToString());
+ }
+
+ /// <summary>
+ /// Writes examples
+ /// </summary>
+ public void WriteExamples()
+ {
+ }
+
+ /// <summary>
+ /// Outputs a formatted string to the console.
+ /// We're not using the ILogger here, since we want console output.
+ /// </summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void Write(string format, params object[] args)
+ {
+ Output.WriteLine(Log, OutputLevel.Information, format, args);
+ }
+
+ /// <summary>
+ /// Outputs an empty line
+ /// </summary>
+ public void WriteLine()
+ {
+ Output.WriteLine(Log, OutputLevel.Information, string.Empty);
+ }
+
+ /// <summary>
+ /// Returns the application (assembly) name (without extension)
+ /// </summary>
+ protected static string ApplicationName
+ {
+ get
+ {
+ return Assembly.GetEntryAssembly().GetName().Name;
+ }
+ }
+
+ /// <summary>
+ /// Returns the application (assembly) version
+ /// </summary>
+ protected static Version ApplicationVersion
+ {
+ get
+ {
+ // Assembly.GetEntryAssembly() is null when loading from the
+ // non-default AppDomain.
+ var a = Assembly.GetEntryAssembly();
+ return a != null ? a.GetName().Version : new Version();
+ }
+ }
+
#endregion
}
}


--
Emanuele Aina
Studio Associato Di Nunzio e Di Gregorio
http://dndg.it/
Via Maria Vittoria, 2
10123 Torino - Italy

0001-Port-Db-Sql-Metal-to-Mono.Options.patch.gz

Jonathan Pryor

unread,
Nov 5, 2009, 8:04:51 AM11/5/09
to dbl...@googlegroups.com
On Wed, 2009-11-04 at 22:19 +0100, Emanuele Aina wrote:
> I've replaced the custom (broken) command line option parsing system
> with Mono.Options.

Hallelujah!

Just one change...

> diff --git a/src/DbMetal/Parameters.cs b/src/DbMetal/Parameters.cs
> index a17514d..4340f0f 100644
> --- a/src/DbMetal/Parameters.cs
> +++ b/src/DbMetal/Parameters.cs

...


> + public void Parse(IList<string> args)
> {
> - return GetParameterBatch<Parameters>(args);
> + Options = new OptionSet() {
> + { "c|conn=", "Database {CONNECTION STRING}. Cannot be used with /server, /user or /password options.", conn => Conn = conn },

Could you use the multi-line formatting convention instead? It makes it
easier to read:

{ "c|conn=",
"Database {CONNECTION STRING}. Cannot be used with /server, /user or /password options.",
conn => Conn = conn },

{ ...

> + { "generate-timestamps|generateTimestamps", "Generate timestampes in the generated code. True by default.", (bool v) => GenerateTimestamps = v },

You can drop the generateTimestamps option, we only need the
generate-timestamps option. (It's only used by one of the DbMetal
tests.)

Thanks!

- Jon


Jonathan Pryor

unread,
Nov 5, 2009, 1:50:24 PM11/5/09
to dbl...@googlegroups.com
On Wed, 2009-11-04 at 22:19 +0100, Emanuele Aina wrote:
> I've replaced the custom (broken) command line option parsing system
> with Mono.Options.

Ooh, one other comment that just occurred to me: you shouldn't use
Parameters.GetArray() for e.g. Parameters.EntityImplementedInterfaces,
as it would be useful for the user to be able to specify the option
multiple times, e.g.

DbMetal --entityInterfaces=IFoo --entityInterfaces=IBar

If they attempt to do so, the 2nd --entityInterfaces will "overwrite"
the first one.

What should instead be done is this:

class Parameters {
// remove public string EntityInterfaces,
// EntityImplementedInterfaces, and instead:
public List<string> EntityImplementedInterfaces = new List<string>();
}

And in the OptionSet:

new OptionSet() {
// ..
{ "entityInterfaces=", "..."
v => EntityImplementedInterfaces.AddRange(
v.Split(new[]{','}, StringSplitOptions.RemoveEmptyEntries)) }
// ...
};

This would allow --entityInterfaces to be invoked multiple times and
work in an *additive* fashion during each invocation.

- Jon


Emanuele Aina

unread,
Nov 7, 2009, 8:21:59 AM11/7/09
to dbl...@googlegroups.com
Jonathan Pryor suggerì:

> Could you use the multi-line formatting convention instead? It makes it
> easier to read:

Done.

> You can drop the generateTimestamps option, we only need the
> generate-timestamps option. (It's only used by one of the DbMetal
> tests.)

Done.

Reworked patch attached.

0001-Port-Db-Sql-Metal-to-Mono.Options.patch.gz

Emanuele Aina

unread,
Nov 7, 2009, 8:27:39 AM11/7/09
to dbl...@googlegroups.com
Jonathan Pryor precisò:

> Ooh, one other comment that just occurred to me: you shouldn't use
> Parameters.GetArray() for e.g. Parameters.EntityImplementedInterfaces,
> as it would be useful for the user to be able to specify the option
> multiple times, e.g.
>
> DbMetal --entityInterfaces=IFoo --entityInterfaces=IBar

My plan was to apply such semantic changes in a different patch, but if
you prefer to have them all together I'll resend the patch.

Maybe should I remove the comma-separated-list options altogether with
the mechanism you described?

As in 'DbMetal --entityInterface=IFoo --entityInterface=IBar' instead of
'DbMetal --entityInterfaces=IFoo,IBar'?

Jonathan Pryor

unread,
Nov 8, 2009, 2:07:08 PM11/8/09
to dbl...@googlegroups.com
On Sat, 2009-11-07 at 14:21 +0100, Emanuele Aina wrote:
> Reworked patch attached.

Commit, but first:

- Processor.cs should follow normal brace conventions, so { should go
on a new line (`if (parameters.Help) {...`).
- Processor.cs should catch Exception, not just OptionException
(as Options will pass-through any intermediate exceptions unwrapped,
e.g. string->int conversions or exceptions thrown from the Action
callbacks).

You'll likely also need to rebase against trunk, as I recently changed
DbMetal to generate both INotifyPropertyChanged and
INotifyPropertyChanging interfaces, and this will impact your patch
(e.g. the Parameters.cs @@ -248... @@ block) and Option documentation
(entityInterfaces option).

Once you've made those changes, you can commit w/o further review.

Thanks!
- Jon


Jonathan Pryor

unread,
Nov 8, 2009, 2:27:18 PM11/8/09
to dbl...@googlegroups.com
On Sat, 2009-11-07 at 14:27 +0100, Emanuele Aina wrote:
> Jonathan Pryor precisò:
> > Ooh, one other comment that just occurred to me: you shouldn't use
> > Parameters.GetArray() for e.g. Parameters.EntityImplementedInterfaces,
> > as it would be useful for the user to be able to specify the option
> > multiple times, e.g.
> >
> > DbMetal --entityInterfaces=IFoo --entityInterfaces=IBar
>
> My plan was to apply such semantic changes in a different patch, but if
> you prefer to have them all together I'll resend the patch.

Good point. Do them as separate patches.

> Maybe should I remove the comma-separated-list options altogether with
> the mechanism you described?
>
> As in 'DbMetal --entityInterface=IFoo --entityInterface=IBar' instead of
> 'DbMetal --entityInterfaces=IFoo,IBar'?

I'm open to other opinions, but I prefer having them as separate values
than as comma-separated values.

That said, mcs seems to do both: have a *singular* option, which can be
specified multiple times, but can ALSO take a comma-separated list of
values, e.g. 'csc -d:foo -d:bar -d:baz,qux'. Ditto with -lib.

So I would suggest following suit: use singular option names, e.g.
--entityInterface instead of --entityInterfaces, but allow multiple
comma-separated values to be used.

I'd also suggest providing shorter option alias, e.g. -i for
entityInterface, -b for entityBase, -ea for entityAttribute, -ma for
memberAttribute. Then again, perhaps not -- longer names can be more
readable.

Thanks,
- Jon


Federico Di Gregorio

unread,
Nov 8, 2009, 2:34:06 PM11/8/09
to dbl...@googlegroups.com
Il giorno dom, 08/11/2009 alle 14.27 -0500, Jonathan Pryor ha scritto:
> I'd also suggest providing shorter option alias, e.g. -i for
> entityInterface, -b for entityBase, -ea for entityAttribute, -ma for
> memberAttribute. Then again, perhaps not -- longer names can be more
> readable.

Can I say that shot options should be limited to one letter? So that it
is possible to group them if necessary. Yes, I know, this is getopt
inheritance but people are so used to it..

federico

--
Federico Di Gregorio http://people.initd.org/fog
Debian GNU/Linux Developer f...@debian.org
INIT.D Developer f...@initd.org
Quando sono a casa a studiare me la sbrigo rapida rapida clitoridea
come quando avevo 11 anni. -- (Obviously) Anonymous

Jonathan Pryor

unread,
Nov 8, 2009, 3:06:07 PM11/8/09
to dbl...@googlegroups.com
On Sun, 2009-11-08 at 20:34 +0100, Federico Di Gregorio wrote:
> Il giorno dom, 08/11/2009 alle 14.27 -0500, Jonathan Pryor ha scritto:
> > I'd also suggest providing shorter option alias, e.g. -i for
> > entityInterface, -b for entityBase, -ea for entityAttribute, -ma for
> > memberAttribute. Then again, perhaps not -- longer names can be more
> > readable.
>
> Can I say that shot options should be limited to one letter? So that it
> is possible to group them if necessary. Yes, I know, this is getopt
> inheritance but people are so used to it..

Mono.Options doesn't care, but restricting to one letter does allow
bundling the value with the option, e.g. -iINotifyPropertyChanged.
Opinions on whether that's a good thing or not will vary. :-)

I'd also suggest only doing that for more frequently used options --
entityAttribute and methodAttribute likely don't need shorter options.
I'm not sure entityInterface and entityBase need shorter options either,
for that matter.

- Jon


Emanuele Aina

unread,
Nov 14, 2009, 9:11:11 AM11/14/09
to dbl...@googlegroups.com
Jonathan Pryor autorizzò:

> Commit, but first:
>
> - Processor.cs should follow normal brace conventions, so { should go
> on a new line (`if (parameters.Help) {...`).
> - Processor.cs should catch Exception, not just OptionException
> (as Options will pass-through any intermediate exceptions unwrapped,
> e.g. string->int conversions or exceptions thrown from the Action
> callbacks).
>
> You'll likely also need to rebase against trunk, as I recently changed
> DbMetal to generate both INotifyPropertyChanged and
> INotifyPropertyChanging interfaces, and this will impact your patch
> (e.g. the Parameters.cs @@ -248... @@ block) and Option documentation
> (entityInterfaces option).
>
> Once you've made those changes, you can commit w/o further review.

Committed as r1272. Hopefully 'git svn dcommit' worked correctly, as
this was my first experience with it. :)
Reply all
Reply to author
Forward
0 new messages