Skip to content

Instantly share code, notes, and snippets.

@steeleprice
Forked from robjens/00.fs
Created February 26, 2019 22:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steeleprice/033bb2592b4f03bafd333dddc081f4d4 to your computer and use it in GitHub Desktop.
Save steeleprice/033bb2592b4f03bafd333dddc081f4d4 to your computer and use it in GitHub Desktop.
XSD to type generation - rewritten in and for F# from C# example
(* Well at least it works now *)
module Types
open System
open System.IO
open System.Collections
open System.Collections.Generic
open System.Reflection
open System.Resources
open System.Linq
open System.Xml
open System.Xml.Serialization
open System.Xml.Schema
open System.CodeDom
open System.CodeDom.Compiler
open System.Collections.Specialized
open System.Collections.ObjectModel
open System.Diagnostics
open System.Threading
open FSharp.Compiler.CodeDom
// side-effect operators
let (!!) f = f |> ignore
let (>>!) a b = Console.WriteLine(a, b.ToString()) |> ignore
// predicates
let inline notNil value = not (obj.ReferenceEquals(value, null))
/// ComplexType XmlSchemaElement
type XmlSchemaComplexType with
member x.attributes =
seq {
for a in x.Attributes do
yield a :?> XmlSchemaAttribute
}
member x.attributeUses =
seq {
for au in x.AttributeUses do
yield au
}
/// File system and input/output operations
type Fsio =
static member slurp (inFile:string) =
use rdr = new StreamReader(inFile)
rdr.ReadToEnd()
/// Spits a string of content to the named output file path.
static member spit outFile content =
IO.File.WriteAllText(outFile, content)
/// Returns the assemblies equivalent of present working directory
static member pwd = Environment.CurrentDirectory
/// Takes a file path (non-root) and prefixes the current assembly working directory.
static member joinPwd path = IO.Path.Combine(Fsio.pwd, path)
//++ Augmentations
//+ XmlSchema(s)
type XmlSchema with
member x.namespaces = x.Namespaces.ToArray()
static member init (txt:string) =
let h = new ValidationEventHandler(fun _ -> failwith "invalid schema")
use rdr = new XmlTextReader(new StringReader(txt))
XmlSchema.Read(rdr, h)
static member load (path:string) =
XmlSchema.init (Fsio.slurp path)
member x.seqSchemaTypes =
seq {
for st in x.SchemaTypes.Values do
yield st :?> XmlSchemaType
}
member x.seqElements =
seq {
for se in x.Elements.Values do
yield se :?> XmlSchemaElement
}
member x.seqAttributes =
seq {
for sa in x.Attributes.Values do
yield sa :?> XmlSchemaAttribute
}
member x.seqItems =
seq {
for si in x.Items do
yield si
}
member x.print =
use ms : MemoryStream = new MemoryStream(1024)
x.Write(ms) // write schema to memory stream
(!!) (ms.Seek(Int64.Parse("0"), SeekOrigin.Begin))
use rdr : StreamReader = new StreamReader(ms)
rdr.ReadToEnd()
member x.spit msg =
Fsio.spit (Fsio.joinPwd msg + ".xsd") x.print
member x.itemProps =
x.seqItems |> Seq.map (fun z -> z.GetType().GetProperties())
member x.itemNames = x.seqItems |> Seq.map (fun item -> item.ToString())
member x.filter pred = x.seqItems |> Seq.filter pred
type XmlSchemaSet with
/// Returns a pristine new XmlSchemaSet instance from a static method call, bind value as needed.
/// Automatically resolves include/import namespace URL's and schemas (e.g. basisschema.xsd)
static member init : XmlSchemaSet =
let this = new XmlSchemaSet()
this.XmlResolver <- new XmlUrlResolver()
this
static member create (xs:XmlSchema) =
let this = XmlSchemaSet.init // delegate
(!!) (this.Add(xs))
this
static member load (msg:string) =
let this = new XmlSchemaSet()
let path = Fsio.joinPwd msg + ".xsd"
(!!) (this.Add("", path))
this
/// Returns a XmlSchemaSet instance with the provided sequence schemas added to it. Uses static
/// method call to init and itself is a static member method. Result is a option maybe value (Some/None)
static member initially schemas : XmlSchemaSet option =
let this = XmlSchemaSet.init
schemas |> Seq.iter (fun (x : XmlSchema) -> !!(this.Add(x)))
this.Compile()
if this.IsCompiled then Some this
else None
/// Returns a list with all schemas from the schema set yielded as values from the collection.
member this.schemas =
[ for xs in this.Schemas() do
yield xs :?> XmlSchema ]
/// Predicate returns true if the schema set does not contain any schemas in it.
member this.isEmpty = this.schemas |> List.isEmpty
/// Returns a fully configured and initialized XmlReaderSettings object set on the current instance.
member this.validationSettings =
let rdrSettings =
XmlReaderSettings
(Schemas = this, ValidationType = ValidationType.Schema,
ValidationFlags = (XmlSchemaValidationFlags.ProcessIdentityConstraints
||| XmlSchemaValidationFlags.ReportValidationWarnings))
rdrSettings.ValidationEventHandler.Add(fun args ->
match args.Severity with
| XmlSeverityType.Warning -> printfn "%A" args.Message
| _ -> printfn "%A" (args.Exception.ToString()))
rdrSettings
/// Static method which takes a list of one or more schemas and a file to validate. Returns generic
/// deserialized results.
static member deserialize (schemas : XmlSchema list) (file : string) func =
try
let this = (XmlSchemaSet.initially schemas |> Option.get)
let conf = this.validationSettings
use input = new StreamReader(file)
use reader = XmlReader.Create(input, conf)
let serializer = XmlSerializer(typeof<'T>)
serializer.Deserialize(reader) :?> 'T |> func
with
| :? XmlException as xe -> (>>!) xe.Message "XML Parse Error"
| :? InvalidOperationException as ioe ->
(>>!) ioe.InnerException.Message "XML Serialization Error"
| e -> (>>!) e.Message "Other Error"
//+ CodeDom
/// Augmentation of CodeAttributeDeclaration
type CodeAttributeDeclaration with
/// Adds a predicate to code attribute decls. which returns true when a XmlTypeAttribute
member x.IsXmlTypeAttribute =
x.Name = "System.Xml.Serialization.XmlTypeAttribute"
/// Augmentation of CodeTypeDeclaration
type CodeTypeDeclaration with
/// Returns a seq of all custom attributes in the code type declaration
member x.customAttrs =
seq {
for ca in x.CustomAttributes do
yield ca
}
/// Returns a array of only those custom attributes which are of type XmlTypeAttribute
static member xmlTypeAttrs z =
z |> Seq.filter (fun (cad:CodeAttributeDeclaration) ->
cad.IsXmlTypeAttribute)
|> List.ofSeq // ffs keep this in mind! PRODUCE A NEW VALUE!!!!
/// Triggers the side effect of clearing custom attributes and then re-adding those selected
/// XmlCodeAttributes which have been selected (filtered) by means of predicate in XmlTypeAttr seq
member x.xmlTypeAttrsOnly =
let xtattr = CodeTypeDeclaration.xmlTypeAttrs x.customAttrs
x.CustomAttributes.Clear()
xtattr |> Seq.iter (fun item -> !!(x.CustomAttributes.Add(item)))
xtattr
/// Augmentation of CodeNamespace type
type CodeNamespace with
/// Initializes a new instance of a CodeNamespace with given namespace string.
static member init ns : CodeNamespace = new CodeNamespace(ns)
/// Creates a XmlCodeExporter set to this instance of a CodeNamespace class
member x.exporter : XmlCodeExporter = new XmlCodeExporter(x)
/// Returns a seq of all code types in the current code namespace
member x.seqCodeTypes =
seq {
for ct in x.Types do
yield ct
}
/// Returns a seq of all code namespace imports in the current code namespace
member x.seqCodeNsImports =
seq {
for ci in x.Imports do
yield ci
}
/// Returns a seq of all code comments in the current code namespace
member x.seqCodeComments =
seq {
for cc in x.Comments do
yield cc
}
/// Augmentation of FSharpCodeProvider
type FSharpCodeProvider with
/// Static constructor (initializer) member method which takes a CodeNamespace to validate identifiers
/// and finally produces a pristine FSharpCodeProvider value each call
static member init ns : FSharpCodeProvider =
CodeGenerator.ValidateIdentifiers(ns)
new FSharp.Compiler.CodeDom.FSharpCodeProvider()
(*
- XmlSchemas on steroids. Augmentation is a powerful concept. A few words: do note that in F# we're not
- allowed to define during augmentation (the type + `with' denotes it) any mutable fields e.g. let mutable foo = "bar"
- often you may find playing down here to take the following forms:
- * static constructors returning a pristine instance of some object
- * side-effects mutating the CURRENT schema instance
- * side-effects called once in other instances, either passed as arguments
- * or destructured as tuppled arguments and partially (pattern matched) bound to local variables
Once a certain object instance is not longer required, or when we know how to retrieve that instance or
when we can call parents/children instead, it is often discarded (foo, _, _) =
Since most operations required for ones own Xsd.exe go by XmlSchemas, monkeypatching it with some
auxiliary static and instance member methods and values makes it heaps easier to work with the intermediate
moving parts.
*)
type XmlSchemas with
/// Augments the same pattern of static methods as XmlSchemaSet. Init will return a single instance.
static member init : XmlSchemas = new XmlSchemas()
/// Takes a set of schemas and adds them to the collection, validating in the meanwhile. Then compiles
/// and returns the results.
static member initially schemas : XmlSchemas =
let this = XmlSchemas.init
let hndl =
new ValidationEventHandler(fun _ ->
failwith "XmlSchema not compiled")
schemas |> Seq.iter (fun (x : XmlSchema) -> !!(this.Add(x)))
this.Compile(hndl, true)
this
/// There is no easy way to retrieve all schemas once inside a (serializer) XmlSchemas collection,
/// however, here we defined a inner recursive function which moves the cursor in the collection
/// as long as needed, returning the value on each successive seed fed to the fn.
/// recursive fn was whacked so this works at least,... took me 1.5 days
member x.schemas : XmlSchema list =
let xse = new XmlSchemaEnumerator(x)
(!!) (xse.MoveNext())
let a = xse.Current
(!!) (xse.MoveNext())
let b = xse.Current
[a; b]
/// Takes a function and applies it to the schemas instance mapping over the member property value
member private x.applyToSchemas fn = x.schemas |> List.map fn
/// Xml schema types flat sequence
member x.schemasTypes =
x.applyToSchemas (fun (xsd : XmlSchema) -> xsd.seqSchemaTypes)
|> Seq.concat // iron it out (flatten)
/// Xml schema elements flat sequence
member x.schemasElements =
x.applyToSchemas (fun (xsd : XmlSchema) -> xsd.seqElements)
|> Seq.concat
/// Instances of popular xml schema to code dom conversions
member x.instances ns : CodeNamespace * XmlSchemaImporter * XmlCodeExporter =
ns |> CodeNamespace.init |> (fun cns -> cns, new XmlSchemaImporter(x), cns.exporter)
/// Imports type mappings, creates a code namespace
member x.import : CodeNamespace * XmlSchemaImporter * XmlCodeExporter =
let nsname = "ZorgMatch.Domain.Wlz"
let (cns, importer, exporter) = x.instances nsname
// mapped schema types imported and stored
let types : XmlTypeMapping list =
x.schemasTypes
|> Seq.toList
|> List.map
(fun (ct : XmlSchemaType) ->
importer.ImportSchemaType(ct.QualifiedName)
)
// elements import and type mapping stored as value
let elems : XmlTypeMapping list =
x.schemasElements
|> Seq.toList
|> List.map
(fun (ce : XmlSchemaElement) ->
importer.ImportTypeMapping(ce.QualifiedName))
// cons lists and export mappings (side-effect)
(types @ elems) |> List.iter (fun cm -> exporter.ExportTypeMapping(cm))
// we can just return all 'old' instances since the iterations mutation those specific objects
(cns, importer, exporter)
/// Performs side-effects of populating the instance schemas contained xml elements, types and attributes
/// to the code dom equivallent objects and returns a triple of code ns, attributes and types.
member x.codeDeclarations =
let ns, _, _ = x.import
let codeTypes = ns.seqCodeTypes
let xattrs =
codeTypes
|> Seq.map
(fun (ct : CodeTypeDeclaration) ->
ct.xmlTypeAttrsOnly) // oops, was missing the -Only (which has side-effects)
|> Seq.concat |> Seq.toList
(ns, xattrs, codeTypes |> Seq.toList)
/// Code provider instance for FSharp generation from Xsd. Contains a tupple including code namespace.
member x.codeProvider : CodeNamespace * FSharpCodeProvider =
let ns, _, _ = x.codeDeclarations
ns, FSharpCodeProvider.init ns
/// Dumps generated F# code to the console.
member x.dumpCode =
let ns, codeProvider = x.codeProvider
use writer : StringWriter = new StringWriter()
codeProvider.GenerateCodeFromNamespace
(ns, writer, new CodeGeneratorOptions())
writer.GetStringBuilder().ToString()
/// Writes the generated F# code to file.
member x.writeCode (path : string) (name : string) =
let ns, codeProvider = x.codeProvider
use writer : StringWriter = new StringWriter()
/// Side effect, mutating the writer object to later retrieve the string builder. The namespace
/// contains the mutated code types and attributes generated from taking in Xml types and values.
codeProvider.GenerateCodeFromNamespace
(ns, writer, new CodeGeneratorOptions())
let outFile = IO.Path.Combine(path, (name.ToString() + ".fs"))
Fsio.spit outFile (writer.GetStringBuilder().ToString())
if IO.File.Exists(outFile) then ("Created " + outFile)
else ("Error, failed to locate generated file at " + outFile)
//++ Custom types
/// Load Xml schemas from a compiled/generated .resources file made
/// from a .resx with all messages embedded
type SchemaResources =
/// Loads a .resources file and returns a map of all schemas inside.
static member load dom =
let env = Environment.CurrentDirectory
let frm = String.Format("{0}.resources", dom.ToString())
let res = IO.Path.Combine(env, frm)
let filename =
match IO.File.Exists(res) with
| true -> res
| false -> failwith ("Resource file not found in " + res)
let stream = File.Open(filename, IO.FileMode.Open)
use rdr : ResourceReader = new Resources.ResourceReader(stream)
seq { for x in rdr do yield x :?> DictionaryEntry }
|> Seq.map (fun de -> de.Key.ToString(), de.Value.ToString())
|> Map.ofSeq
/// Sets-up XmlSchema for a message and has a XmlSchemaSet add and compile them.
// static member setup =
// XmlSchema.init >> XmlSchemaSet.slurp
/// Takes a domain e.g. iWlz10 and a message key e.g. IO31 and returns the schema resources.
static member init dom msg =
let text = (SchemaResources.load dom).[msg]
let xs = new XmlSchemas()
let a = XmlSchema.load (Fsio.joinPwd "basisschema.xsd")
let b = text |> XmlSchema.init
(!!) (xs.Add(a))
(!!) (xs.Add(b))
let hndl = new ValidationEventHandler(fun _ -> failwith "XmlSchema not compiled")
xs.Compile(hndl, true)
xs
// printfn "%A" text
// let schemas = setup.schemas |> XmlSchemas.initially
// schemas
// (Fsio.joinPwd msg + ".xsd")
// o.spit msg // persist to working directory since we might need a physical file after all

XSD to F# types

Note
There are two methods that I know of and have both worked for me. I will describe each of them in this document and expand as I find more (of poorly documented/blogged) codedom augmenting of the arcane xsd.exe program.

Reference(s): . Creating a Key Pair

  1. create a new F# console project, name it e.g. like the one we’ll hack into FSharp.Compiler.CodeDom in a given location anywhere on the machine, let’s say C:\data\

  2. manage nuget package references for the project and search on codedom, then select and click install for FSharp.Compiler.CodeDom

  3. the dll (third party, unsigned) is now located in C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40\ with two files:

    1. FSharp.Compiler.CodeDom.dll

    2. FSharp.Compiler.CodeDom.pdb

Using PowerShell:

  1. cd into the directory C:\data\Fsharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40\

  2. create a key pair using sn -k keypair.snk

PS C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40> sn -k keypair.snk

Microsoft (R) .NET Framework Strong Name Utility  Version 4.0.30319.33440 (1)
Copyright (c) Microsoft Corporation.  All rights reserved.

Key pair written to keypair.snk

PS C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40>
  1. if your version doesn’t match that of the target third party .dll, first ensure that it does or you won’t be able to make it work

We’ll use Microsoft intermediate language Disassembler or (Ildasm) to disassemble the .dll to a .il file first:

  1. execute the command ildasm.exe from that directory, and it will open the disassembler IL DAsm

  2. open a file and the file dialog will be in that current folder, then select the .dll

  3. now click File again and select dump menu option

  4. just keep all options as they are (utf-8 etc) or tweak if you know what you are doing (which is rare, so congrats :)

  5. name it exactly as the .dll only - extension so ` FSharp.Compiler.CodeDom` would be that name, it adds the .il extension itself

  6. the program can be closed and return back to the shell

You should by now have the files as:

..\lib\net40> ls -Name

FSharp.Compiler.CodeDom.dll
FSharp.Compiler.CodeDom.il
FSharp.Compiler.CodeDom.pdb
FSharp.Compiler.CodeDom.res
FSharpOptimizationData.FSharp.Compiler.CodeDom
FSharpSignatureData.FSharp.Compiler.CodeDom
keypair.snk
  1. note the .res file, it is important to include this when assembling back the .il (intermediate language) to a usable (to be installed in GAC global assembly cache) dll you can reference to from xsd.exe

  2. rename or backup your original third party assembly: mkdir bak; cp *.dll bak

  3. next ensure you have the (probable to me, you should ensure, this was mine) location: C:\Windows\Microsoft.NET\Framework\v4.0.30319 directory to the environment variables Path variable (if you want to use it from anywhere in the hierachy of folders)

  4. then issue ilasm.exe /dll /key=keypair.snk FSharp.Compiler.CodeDom.il

Note
Expect a large amount of data to be dumped in the console, the output below is for obvious reasons a truncated form
<...>

Class 2291
Class 2292
Method Implementations (total): 30
Resolving local member refs: 0 -> 0 defs, 0 refs, 0 unresolved
Writing PE file
Signing file with strong name
Operation completed successfully

PS C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40>

You should verify this using:

PS C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40> sn -vf .\FSharp.Compiler.CodeDom.dll

Microsoft (R) .NET Framework Strong Name Utility  Version 4.0.30319.33440
Copyright (c) Microsoft Corporation.  All rights reserved.

Assembly '.\FSharp.Compiler.CodeDom.dll' is valid
  1. ensure you have a reference in your path for the corresponding folders of gacutil (likely in C:\Program Files (x86)\Microsoft SDKs\Windows\8.1A\bin\<[SOMETHING]>

Next we can add the assembly to the global cache and it won’t complain anymore about not being signed, use the /i switch and the name of the dll as value like below:

PS C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40> gacutil.exe /i .\FSharp.Compiler.CodeDom.dll
Microsoft (R) .NET Global Assembly Cache Utility.  Version 4.0.30319.33440
Copyright (c) Microsoft Corporation.  All rights reserved.

Assembly successfully added to the cache

Finally, validate the install and write down the relevant pieces for xsd.exe.

PS C:\data\FSharp.Compiler.CodeDom\packages\FSharp.Compiler.CodeDom.0.9.2\lib\net40> gacutil.exe  /l FSharp.Compiler.CodeDom
Microsoft (R) .NET Global Assembly Cache Utility.  Version 4.0.30319.33440
Copyright (c) Microsoft Corporation.  All rights reserved.

The Global Assembly Cache contains the following assemblies:
  FSharp.Compiler.CodeDom, Version=0.9.2.0, Culture=neutral, PublicKeyToken=ca272b5a751b4573, processorArchitecture=MSIL

Number of items = 1

The line below The Global Assembly Cache contains the following assemblies: is used to feed to the /l switch of xsd.exe as follows:

PS C:\code\zm\schemas\iWlz10> xsd.exe .\basisschema.xsd .\IO31.xsd /c /l:"FSharp.Compiler.CodeDom.FSharpCodeProvider, FSharp.Compiler.CodeDom, Version=0.9.2.0, Culture=neutral, PublicKeyToken=ca272b5a751b4573"
Microsoft (R) Xml Schemas/DataTypes support utility
[Microsoft (R) .NET Framework, Version 4.0.30319.33440]
Copyright (C) Microsoft Corporation. All rights reserved.
Writing file 'C:\code\zm\schemas\iWlz10\IO31.fs'.

Tada! That was the first way…​ The F# inline IDE method is below but being worked at and a rough translation of the original C# code to make it work…​

Note
When using the /class or /c option, using multiple files with a base schema on the head (which will prevent duplication of code, aka DRY violations because all files are taken in at once) will create a .cs or .fs in our case for each .xsd. The /dataset or /d switch will *create a single file named after the last file in the list of input .xsd` so that is note worthy as well I guess.
(* This still might need some work not even tested yet !!! *)
namespace Flexo
open System
open System.IO
open System.Collections.Generic
open System.Reflection
open System.Text
open System.Xml
open System.Xml.Serialization
open System.Xml.Schema
open System.CodeDom
open System.CodeDom.Compiler
module XsdToFsharp =
// Remove all the attributes from each type in the CodeNamespace, except
// System.Xml.Serialization.XmlTypeAttribute
let removeAttributes (ns : CodeNamespace) =
let yieldTypeDecls (coll:CodeTypeDeclarationCollection) =
seq { for ct in coll do yield ct }
let yieldAttrDecls (coll:CodeAttributeDeclarationCollection) =
seq { for ca in coll do yield ca }
ns.Types
|> yieldTypeDecls
|> Seq.map (fun ct ->
ct.CustomAttributes
|> yieldAttrDecls
|> Seq.map (fun ca ->
printfn "%A" ca.Name
ct.CustomAttributes.Clear()
match ca.Name with
| "System.Xml.Serialization.XmlTypeAttribute"
-> ct.CustomAttributes.Add(ca)
| _ -> 0
))
let mutable isVerbose = false
let w8() = do Console.ReadLine() |> ignore
//+ preamble
let xsdFileName : string = "basisschema.xsd"
let filePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
let xsdPath : string = Path.Combine(filePath, xsdFileName)
let xsd : XmlSchema =
use stream : FileStream =
new FileStream(xsdPath, FileMode.Open, FileAccess.Read)
( XmlSchema.Read(stream, null) )
if isVerbose then
(printfn "xsd.IsCompiled %A" xsd.IsCompiled)
//+ create and populate
let schemas : XmlSchemas = new XmlSchemas()
do
schemas.Add(xsd) |> ignore
schemas.Compile(null, true)
let codeNs : CodeNamespace = new CodeNamespace("Generated")
let imp : XmlSchemaImporter = new XmlSchemaImporter(schemas)
let exp : XmlCodeExporter = new XmlCodeExporter(codeNs)
let mapper : List<XmlTypeMapping> = new List<XmlTypeMapping>()
//+ schema types
let listTypes =
seq { for obj in xsd.SchemaTypes.Values do
yield obj :?> XmlSchemaType }
let importSchemaTypes coll =
coll |> Seq.iter (fun (st:XmlSchemaType) ->
mapper.Add(imp.ImportSchemaType(st.QualifiedName)))
//+ schema elements
let listElements =
seq { for obj in xsd.Elements.Values do
yield obj :?> XmlSchemaElement }
let importSchemaElements coll =
coll |> Seq.iter (fun (xse:XmlSchemaElement) ->
mapper.Add(imp.ImportTypeMapping(xse.QualifiedName)))
//+ ex-/import
let exportMaps coll =
Seq.iter (fun (m:XmlTypeMapping) -> exp.ExportTypeMapping(m)) coll
do //! execute
importSchemaTypes listTypes
importSchemaElements listElements
exportMaps mapper
//- check for invalid characters in identifier name symbols
CodeGenerator.ValidateIdentifiers(codeNs)
removeAttributes(codeNs)
|> ignore
// output the F# code
let codeProvider : FSharp.Compiler.CodeDom.FSharpCodeProvider =
new FSharp.Compiler.CodeDom.FSharpCodeProvider()
let writeOut =
let opt = new CodeGeneratorOptions()
use sw : StringWriter = new StringWriter()
codeProvider.GenerateCodeFromNamespace(codeNs, sw, opt)
if isVerbose then
printfn "%A" (sw.GetStringBuilder().ToString())
(w8())
[<EntryPoint>]
let main argv =
printfn "%A" argv
exit(0)
# Quick Script for Generation
# (long time since I worked PS so bear with me)
$fsharp = "FSharp.Compiler.CodeDom.FSharpCodeProvider, FSharp.Compiler.CodeDom, Version=0.9.2.0, Culture=neutral, PublicKeyToken=ca272b5a751b4573"
$huge = "basisschema_AW310_AW317_AW318_AW33_AW34_AW35_AW36_AW39_CA317_CA318_CA319_CA320_IO31_IO35_IO36_ZK310_ZK33_ZK34_ZK35_ZK36_ZK39.fs"
$projectDir = "C:\code\zm\project\ZorgMatch.Support.Excite\ZorgMatch.Support.Excite"
Clear-Host
$dryRun = 0
$profile = 0
$startDir = $pwd
$schemaDir = "schemas\iWlz10\"
if (Test-Path $schemaDir)
{
Remove-Generated "schemas\iWlz10" "fs" "xsd"
Gen-Code $schemaDir "basisschema.xsd" "/c" $fsharp "ZorgMatch.Domain.iWlz" $projectDir
cd $startDir
}
# http://www.ravichaganti.com/blog/powershell-ise-addon-validate-and-format-xml/
#Format-XML (Get-Content ".\iWlz10\basisschema.xsd")
function Format-XML
{
param (
[xml]$xml,
$indent = 4
)
$StringWriter = New-Object System.IO.StringWriter
$XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter
$xmlWriter.Formatting = "indented"
$xmlWriter.Indentation = $Indent
$xml.WriteContentTo($XmlWriter)
$XmlWriter.Flush()
$StringWriter.Flush()
return $StringWriter.ToString()
}
function Remove-Generated {
<#
Takes a TargetDir and checks in that target directory if TargetType files are present and
if so, where they have a corresponding file of type SourceType. If so, it removes the TargetType
files.
#>
param (
[string]$TargetDir,
[string]$TargetType,
[string]$SourceType
)
cd ${schemaDir}
$allFiles = ls "*.$TargetType" -Name
foreach ($file in $allFiles) {
$checkAgainst = $file -replace $TargetType, $SourceType
if (Test-Path $checkAgainst) {
if ($dryRun) {
Write-Host "(dry run) File ${checkAgainst} source exists, would delete generated file ${file}"
} else {
rm $file
Write-Host "(deleted) generated code file ${file} - source copy was found"
}
}
}
if (Test-Path $huge)
{
rm $huge
}
if (Test-Path "Wlz.fs")
{
rm "Wlz.fs"
}
cd $startDir
}
function Gen-Code {
<#
ObjectType "/c" classes from schema
"/d" sub-classed DataSet from schema
Language "/l" default CS
#>
param (
[string]$SourceDir,
[string]$MainSchema,
[string]$ObjectType,
[string]$Language,
[string]$Namespace,
[string]$OutDir
)
if ($profile)
{
$sw = [Diagnostics.Stopwatch]::StartNew()
}
cd ${schemaDir}
# get directory content list
$tail = ls "*.xsd" -Name
# drop the main schema from the rest (somehow exclude on the ls stops working due to * wildcard - I guess)
$RestFiles = $tail -creplace $MainSchema
Write-Host
if ($dryRun) {
Write-Host "(dry run) Using directory: ${OutDir}"
Write-Host "(dry run) Found tail/rest files: ${RestFiles}"
Write-Host "(dry run) Cons head schema file: ${MainSchema}"
Write-Host "(dry run) Using language: ${Language}"
Write-Host "(dry run) Using namespace: ${Namespace}"
Get-Content "${projectDir}/Program.fs" | Where-Object { $_.Contains("namespace") }
} else {
xsd.exe ${MainSchema} ${tail} $ObjectType "/l:${Language}" "/n:${Namespace}" "/o:${OutDir}"
}
cd $startDir
if (Test-Path $huge)
{
if (Test-Path "Wlz.fs")
{
rm "Wlz.fs"
}
mv $huge "Wlz.fs"
}
if ($profile)
{
$sw.Stop()
$sw.Elapsed
}
}
(* Some initial ground work... is it evil augmenting XmlSchemas like that?? ^^ *)
module Types
open System
open System.IO
open System.Collections
open System.Collections.Generic
open System.Reflection
open System.Resources
open System.Linq
open System.Xml
open System.Xml.Serialization
open System.Xml.Schema
open System.CodeDom
open System.CodeDom.Compiler
open System.Collections.Specialized
open System.Collections.ObjectModel
open System.Diagnostics
open System.Threading
// side-effects operator (bit like Clojure exclamations bc in F# , is reserved for future use as suffix to identifiers)
let (!!) f = f |> ignore
let (>>!) a b = Console.WriteLine(a, b.ToString()) |> ignore
// predicates
let inline notNil value = not (obj.ReferenceEquals(value, null))
type XmlSchemaComplexType with
member x.AAttributes =
seq {
for a in x.Attributes do
yield a :?> XmlSchemaAttribute
}
member x.AAttributeUses =
seq {
for au in x.AttributeUses do
yield au
}
// Discriminated union
type XsEntities =
| XsElement of XmlSchemaElement
| XsComplexType of XmlSchemaComplexType
| XsSimpleContent of XmlSchemaSimpleContent
| XsContentExtension of XmlSchemaSimpleContentExtension
let mEntities (x : obj) =
match x with
| :? XmlSchemaElement -> printfn "xml element"
| :? XmlSchemaComplexType -> printfn "complex type"
| :? XmlSchemaSimpleContent -> printfn "simple content"
| _ -> printfn "some other type"
type App =
static member DirOut = Environment.CurrentDirectory
static member JoinOut frm = IO.Path.Combine(App.DirOut, frm)
static member Resources file =
let fp = String.Format("{0}.resources", file.ToString()) |> App.JoinOut
match IO.File.Exists(fp) with
| true -> fp
| false -> failwith "Could not locate requested .resources file"
// let cnf = IO.SearchOption.AllDirectories
// let dir = (IO.Directory.EnumerateFiles(env, "*.*", cnf).ToArray())
//+ Augmentations
// Enrich the .NET types with some of our own often used operations and members producing
// values and/or side-effects as required.
type XmlSchema with
member x.ANamespaces = x.Namespaces.ToArray()
member x.seqSchemaTypes =
seq {
for st in x.SchemaTypes.Values do
yield st :?> XmlSchemaType
}
member x.seqElements =
seq {
for se in x.Elements.Values do
yield se :?> XmlSchemaElement
}
member x.seqAttributes =
seq {
for sa in x.Attributes.Values do
yield sa :?> XmlSchemaAttribute
}
member x.seqItems =
seq {
for si in x.Items do
yield si
}
member x.itemProps =
x.seqItems |> Seq.map (fun z -> z.GetType().GetProperties())
member x.itemNames = x.seqItems |> Seq.map (fun item -> item.ToString())
member x.filter pred = x.seqItems |> Seq.filter pred
type XmlSchemaSet with
/// Returns a pristine new XmlSchemaSet instance from a static method call, bind value as needed.
/// Automatically resolves include/import namespace URL's and schemas (e.g. basisschema.xsd)
static member init : XmlSchemaSet =
let this = new XmlSchemaSet()
this.XmlResolver <- new XmlUrlResolver()
this
/// Returns a XmlSchemaSet instance with the provided sequence schemas added to it. Uses static
/// method call to init and itself is a static member method. Result is a option maybe value (Some/None)
static member initially schemas : XmlSchemaSet option =
let this = XmlSchemaSet.init
schemas |> Seq.iter (fun (x : XmlSchema) -> !!(this.Add(x)))
this.Compile()
if this.IsCompiled then Some this
else None
/// Returns a list with all schemas from the schema set yielded as values from the collection.
member this.schemas =
[ for xs in this.Schemas() do
yield xs :?> XmlSchema ]
/// Predicate returns true if the schema set does not contain any schemas in it.
member this.isEmpty = this.schemas |> List.isEmpty
/// Returns a fully configured and initialized XmlReaderSettings object set on the current instance.
member this.validationSettings =
let rdrSettings =
XmlReaderSettings
(Schemas = this, ValidationType = ValidationType.Schema,
ValidationFlags = (XmlSchemaValidationFlags.ProcessIdentityConstraints
||| XmlSchemaValidationFlags.ReportValidationWarnings))
rdrSettings.ValidationEventHandler.Add(fun args ->
match args.Severity with
| XmlSeverityType.Warning -> printfn "%A" args.Message
| _ -> printfn "%A" (args.Exception.ToString()))
rdrSettings
/// Static method which takes a list of one or more schemas and a file to validate. Returns generic
/// deserialized results.
static member deserialize (schemas : XmlSchema list) (file : string) func =
try
let this = (XmlSchemaSet.initially schemas |> Option.get)
let conf = this.validationSettings
use input = new StreamReader(file)
use reader = XmlReader.Create(input, conf)
let serializer = XmlSerializer(typeof<'T>)
serializer.Deserialize(reader) :?> 'T |> func
with
| :? XmlException as xe -> (>>!) xe.Message "XML Parse Error"
| :? InvalidOperationException as ioe ->
(>>!) ioe.InnerException.Message "XML Serialization Error"
| e -> (>>!) e.Message "Other Error"
type CodeAttributeDeclaration with
/// Adds a predicate to code attribute decls. which returns true when a XmlTypeAttribute
member x.IsXmlTypeAttribute =
x.Name = "System.Xml.Serialization.XmlTypeAttribute"
type CodeTypeDeclaration with
/// Returns a seq of all custom attributes in the code type declaration
member x.seqCustomAttributes =
seq {
for ca in x.CustomAttributes do
yield ca
}
/// Returns a array of only those custom attributes which are of type XmlTypeAttribute
member x.seqXmlTypeAttributes =
x.seqCustomAttributes |> Seq.filter (fun cad -> cad.IsXmlTypeAttribute)
member x.xmlTypeAttrsOnly =
let xtattr = x.seqXmlTypeAttributes
x.CustomAttributes.Clear()
xtattr |> Seq.iter (fun item -> !!(x.CustomAttributes.Add(item)))
type CodeNamespace with
/// Initializes a new instance of a CodeNamespace with given namespace string.
static member init ns : CodeNamespace = new CodeNamespace(ns)
/// Creates a XmlCodeExporter set to this instance of a CodeNamespace class
member x.export : XmlCodeExporter = new XmlCodeExporter(x)
/// Returns a seq of all code types in the current code namespace
member x.seqCodeTypes =
seq {
for ct in x.Types do
yield ct
}
/// Returns a seq of all code namespace imports in the current code namespace
member x.seqCodeNsImports =
seq {
for ci in x.Imports do
yield ci
}
/// Returns a seq of all code comments in the current code namespace
member x.seqCodeComments =
seq {
for cc in x.Comments do
yield cc
}
//! Steroids (augment) XmlSchemas including the whole chain of operations leading up
//! to code generation itself. (TWEAK)
type XmlSchemas with
/// Augments the same pattern of static methods as XmlSchemaSet. Init will return a single instance.
static member init : XmlSchemas = new XmlSchemas()
/// Takes a set of schemas and adds them to the collection, validating in the meanwhile. Then compiles
/// and returns the results.
static member initially schemas : XmlSchemas =
let this = XmlSchemas.init
let hndl =
new ValidationEventHandler(fun _ ->
failwith "XmlSchema not compiled")
schemas |> Seq.iter (fun (x : XmlSchema) -> !!(this.Add(x)))
this.Compile(hndl, true)
this
/// There is no easy way to retrieve all schemas once inside a (serializer) XmlSchemas collection,
/// however, here we defined a inner recursive function which moves the cursor in the collection
/// as long as needed, returning the value on each successive seed fed to the fn.
member x.schemas : XmlSchema list =
let xse = new XmlSchemaEnumerator(x)
let rec inner cont seed list =
match cont with
| false -> list
| true -> inner (xse.MoveNext()) xse list @ [ xse.Current ]
inner true xse []
member private x.applyToSchemas fn = x.schemas |> List.map fn
member x.schemasTypes =
x.applyToSchemas (fun (xsd : XmlSchema) -> xsd.seqSchemaTypes)
|> Seq.concat // iron it out (flatten)
member x.schemasElements =
x.applyToSchemas (fun (xsd : XmlSchema) -> xsd.seqElements)
|> Seq.concat
/// Creates a XmlSchemaImporter set to this instance of XmlSchemas class
member private x.importerInst : XmlSchemaImporter = new XmlSchemaImporter(x)
/// Imports type mappings, creates a code namespace
member x.import : CodeNamespace =
let cns = CodeNamespace.init ("Zm.Foo")
let exporter = cns.export
let importer = x.importerInst
// mapped schema types imported and stored
let types : XmlTypeMapping list =
x.schemasTypes
|> Seq.toList
|> List.map
(fun (ct : XmlSchemaType) ->
importer.ImportSchemaType(ct.QualifiedName))
// elements import and type mapping stored as value
let elems : XmlTypeMapping list =
x.schemasElements
|> Seq.toList
|> List.map
(fun (ce : XmlSchemaElement) ->
importer.ImportTypeMapping(ce.QualifiedName))
(// conj and export mappings (side-effect)
types @ elems) |> List.iter (fun cm -> exporter.ExportTypeMapping(cm))
cns // close over code ns mutated
member x.codeTypeDecls =
let ns = x.import
(ns, ns.seqCodeTypes)
member x.codeAttrDecls =
let (ns, ctd) = x.codeTypeDecls
let xattrs =
ctd |> Seq.map
(fun (ct : CodeTypeDeclaration) ->
ct.seqXmlTypeAttributes |> ignore)
(ns, xattrs)
member x.codeProvider : FSharp.Compiler.CodeDom.FSharpCodeProvider =
let (ns, cad) = x.codeAttrDecls
CodeGenerator.ValidateIdentifiers(ns)
new FSharp.Compiler.CodeDom.FSharpCodeProvider()
member x.dumpCode =
use writer : StringWriter = new StringWriter()
x.codeProvider.GenerateCodeFromNamespace(x.import, writer, new CodeGeneratorOptions())
writer.GetStringBuilder().ToString()
(*
let MapResources (path:string) : Map<string, _> =
let buildr = (fun (txt:string) ->
let h = new ValidationEventHandler(fun _ -> failwith "invalid schema")
use rdr = new XmlTextReader(new StringReader(txt))
XmlSchema.Read(rdr, h))
let stream = File.Open(path, IO.FileMode.Open)
use resxRdr : ResourceReader = new Resources.ResourceReader(stream)
[| for x in resxRdr do yield x :?> DictionaryEntry |]
member x.writeCode (path:string) =
use writer : StringWriter = new StringWriter()
x.codeProvider.GenerateCodeFromNamespace(x.import, writer, new CodeGeneratorOptions())
let outFile = String.Format("{0}\{1}.fs", path, id.ToString())
IO.File.WriteAllText(outFile, writer.GetStringBuilder().ToString())
if IO.File.Exists(outFile) then ("Created " + outFile)
else ("Error, failed to locate generated file at " + outFile)
*)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment