Saturday, November 24, 2007

Immutable data class generator: it lives!

Well, it works. You invoke like this:

. .\Library.DataClass.ps1

class MyClass {

    field ([string]) S

    field ([int]) I

} > MyClass.cs

And then you can write:

MyClass mc = new MyClass.Builder().SetS("xxx").ToMyClass();

MyClass mc2 = new MyClass.Builder(mc).SetS("yyy").ToMyClass();

And the output looks like:

internal partial class MyClass

{

public readonly System.String S;

public readonly System.Int32 I;


 

public MyClass(string S, int I)

{

this.S = S;

this.I = I;

}

public class Builder

{

public string S;

public int I;

public Builder()

{

}

public Builder(MyClass value)

{

this.S = value.S;

this.I = value.I;

}

public virtual MyClass ToMyClass()

{

return new MyClass(this.S, this.I);

}

public virtual Builder SetS(string value)

{

this.S = value;

return this;

}

public virtual Builder SetI(int value)

{

this.I = value;

return this;

}

}

}

The main issue is that the ToMyClass and Set* methods are virtual. I think I may be hitting a bug in PowerShell, but I'm still researching it. For now, this will have to do.

Here's the implementation of Library.DataClass.ps1:

function Class

{

    param (

        [String] $name,

        [ScriptBlock] $memberScriptBlock

    )


 

    $class = New-Object System.CodeDom.CodeTypeDeclaration $name

    $class.TypeAttributes = [System.Reflection.TypeAttributes]::NotPublic

    $class.IsPartial = $true


 

    $constructor = New-Object System.CodeDom.CodeConstructor

    $constructor.Attributes = [System.CodeDom.MemberAttributes]::Public

    $class.Members.Add( $constructor ) | Out-Null

    

    $builderClass = New-Object System.CodeDom.CodeTypeDeclaration "Builder"

    $class.Members.Add( $builderClass ) | Out-Null


 

    $builderConstructor = New-Object System.CodeDom.CodeConstructor

    $builderConstructor.Attributes = [System.CodeDom.MemberAttributes]::Public

    $builderClass.Members.Add( $builderConstructor ) | Out-Null

    

    $builderConstructor2 = New-Object System.CodeDom.CodeConstructor

    $builderConstructor2.Attributes = [System.CodeDom.MemberAttributes]::Public

    $builderConstructor2.Parameters.Add(

        (New-Object System.CodeDom.CodeParameterDeclarationExpression( $name, "value" ))

    ) | Out-Null

    $builderClass.Members.Add( $builderConstructor2 ) | Out-Null

    

    $realizeMethod = New-Object System.CodeDom.CodeMemberMethod

    $realizeMethod.Attributes = [System.CodeDom.MemberAttributes]::Public

    $realizeMethod.Name = "To$name"

    $realizeMethod.ReturnType = $name


 

    $ctorExpression = New-Object System.CodeDom.CodeObjectCreateExpression

    $ctorExpression.CreateType = New-Object System.CodeDom.CodeTypeReference($name)

    $realizeMethod.Statements.Add(

        (New-Object System.CodeDom.CodeMethodReturnStatement(

            $ctorExpression

        ))

    ) | Out-Null

        

    $builderClass.Members.Add( $realizeMethod ) | Out-Null


 

    # return a hash of the CodeDom objects related to

    # this field

    function field

    {

        param (

            [Type] $type,

            [String] $name

        )

        

        @{

            type = $type

            name = $name

            readonlyFieldDeclaration = $(

# CodeDom doesn't support 'readonly' fields.

# See http://blogs.msdn.com/bclteam/archive/2005/03/16/396915.aspx

#                $field = New-Object System.CodeDom.CodeMemberField($type, $name)

#                $field.Attributes = [System.CodeDom.MemberAttributes]::Public

#                $field

                New-Object System.CodeDom.CodeSnippetTypeMember("`tpublic readonly $type $name;`n")

                )

            fieldDeclaration = $(

                $field = New-Object System.CodeDom.CodeMemberField($type, $name)

                $field.Attributes = [System.CodeDom.MemberAttributes]::Public

                $field

            )

            parameter = New-Object System.CodeDom.CodeParameterDeclarationExpression($type, $name)

            fieldReference = New-Object System.CodeDom.CodeFieldReferenceExpression(

                (New-Object System.CodeDom.CodeThisReferenceExpression),

                $name

            )

            parameterReference = New-Object System.CodeDom.CodeVariableReferenceExpression $name

            setMethodName = "Set$name"

        }                

    }

    

    & $memberScriptBlock | foreach {

        $class.Members.Add( $_.readonlyFieldDeclaration )

        

        $constructor.Parameters.Add( $_.parameter )


 

        $constructor.Statements.Add(

            $(New-Object System.CodeDom.CodeAssignStatement( $_.fieldReference, $_.parameterReference ))

        )

        

        $builderConstructor2.Statements.Add(

            (New-Object System.CodeDom.CodeAssignStatement(

                $_.fieldReference,

                (New-Object System.CodeDom.CodeFieldReferenceExpression(

                    (New-Object System.CodeDom.CodeVariableReferenceExpression "value"),

                    $_.name

                ))

            ))

        )

        

        $builderClass.Members.Add( $_.fieldDeclaration ) | Out-Null

        $setMethod = New-Object System.CodeDom.CodeMemberMethod

        $setMethod.Name = $_.setMethodName


 

        $setMethod.ReturnType = $builderClass.Name

        # This should be Public,Final, but that fails for me. Possible PowerShell bug?

        $setMethod.Attributes = [System.CodeDom.MemberAttributes] "Public"

        $setMethod.Statements.Add(

            (New-Object System.CodeDom.CodeAssignStatement(

                $_.fieldReference,

                (New-Object System.CodeDom.CodeVariableReferenceExpression ("value"))

            ))

        )

        $setMethod.Statements.Add(

            (New-Object System.CodeDom.CodeMethodReturnStatement(

                (New-Object System.CodeDom.CodeThisReferenceExpression)

            ))

        )

        

        $setMethod.Parameters.Add(

            (New-Object System.CodeDom.CodeParameterDeclarationExpression( $_.type, "value" ))

        )

        $builderClass.Members.Add( $setMethod )

        

        $ctorExpression.Parameters.Add( $_.fieldReference )

        

    } | Out-Null    

    

    $csharpCodeProvider = New-Object Microsoft.CSharp.CSharpCodeProvider

    $sw = New-Object System.IO.StringWriter

    $codeGeneratorOptions = New-Object System.CodeDom.Compiler.CodeGeneratorOptions

    $codeGeneratorOptions.BracingStyle = "C"

    $codeGeneratorOptions.BlankLinesBetweenMembers = $false

    

    $csharpCodeProvider.GenerateCodeFromType( $class, $sw, $codeGeneratorOptions )

    

    $sw.ToString()

}

No comments:

 
Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.