A DSL to generate XSD files

Because of the enormous amount of duplication in XSD files, writing them by hand to can be very tedious. There are tools to reduce the pain, but they are usually quite expensive and still require quite a bit of manual work.

MJ - I have written a small DSL in Tcl to automate the largest part of creating an XSD. I call it TSD for Tcl Schema Definition

The DSL is fairly straightforward For example this tsd file:

text {<?xml version="1.0" encoding="UTF-8"?>}
text {<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">}

define root item+
define item { part1!  part2?  part3 }
define part1 xsd:string
define part2 xsd:decimal {This is optional}
extension part3 xsd:normalizedString {{attr1 {this is attr1}}} { Complex stuff }

text "</xsd:schema>"

will be translated to:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
  <xsd:element name="root" type="rootType"/>
  <xsd:complexType name="rootType">
    <xsd:sequence>
      <xsd:element minOccurs="1" maxOccurs="unbounded" ref="item"/>
    </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="item" type="itemType"/>
  <xsd:complexType name="itemType">
    <xsd:sequence>
      <xsd:element minOccurs="1" maxOccurs="1" ref="part1"/>
      <xsd:element minOccurs="0" maxOccurs="1" ref="part2"/>
      <xsd:element ref="part3"/>
    </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="part1" type="xsd:string"/>
  <xsd:element name="part2" type="xsd:decimal">
    <xsd:annotation>
      <xsd:documentation>This is optional</xsd:documentation>
    </xsd:annotation>
  </xsd:element>
  <xsd:element name="part3" type="part3Type"/>
  <xsd:complexType name="part3Type">
    <xsd:annotation>
      <xsd:documentation> Complex stuff </xsd:documentation>
    </xsd:annotation>
    <xsd:simpleContent>
      <xsd:extension base="xsd:normalizedString">
        <xsd:attribute name="attr1" use="optional">
          <xsd:annotation>
            <xsd:documentation>this is attr1</xsd:documentation>
          </xsd:annotation>
        </xsd:attribute>
      </xsd:extension>
    </xsd:simpleContent>
  </xsd:complexType>
</xsd:schema>

The script:

package require Tcl 8.5
package require tdom

proc define {element sequence {documentation {}}} {
    if {  [llength $sequence] == 1 &&
          [string range $sequence 0 3] eq "xsd:"
    } {
        emitElement $element $sequence $documentation
        return
    }  
    set typename ${element}Type
    emitElement $element $typename 
    emitTypeDef $typename $sequence $documentation
}  

proc extension {element baseType attributes {documentation {}}} {
    emit [subst {<xsd:element name="$element" type="${element}Type"/>
<xsd:complexType name="${element}Type">
    <xsd:annotation>
        <xsd:documentation>$documentation</xsd:documentation>
    </xsd:annotation>
    <xsd:simpleContent>
        <xsd:extension base="$baseType">
    }]  
    foreach attribute $attributes {
        lassign $attribute name doc 
        emit [ subst {    <xsd:attribute name="$name" use="optional">
                <xsd:annotation>
                    <xsd:documentation>$doc</xsd:documentation>
                </xsd:annotation>
            </xsd:attribute>}]
            
    }
    emit {
            </xsd:extension>
    </xsd:simpleContent>
</xsd:complexType>}

}

proc text text {
    emit $text\n
}

proc include {filename} {
    set f [open $filename]
    fconfigure $f -encoding utf-8
    emit \n[read $f]\n
    close $f
}

proc emitElement {element type {documentation {}}} {
    if {$element eq {}} {
        set cardinality {}
        switch -- [string index $type end] {
            + {
                set type [string range $type 0 end-1 ]
                set cardinality {minOccurs="1" maxOccurs="unbounded"}
            }
            ? {
                set type [string range $type 0 end-1 ]
                set cardinality {minOccurs="0" maxOccurs="1"}
            }
            ! {
                set type [string range $type 0 end-1 ]
                set cardinality {minOccurs="1" maxOccurs="1"}
            }
        }
        emit "<xsd:element $cardinality ref=\"$type\">\n"
    } {
        emit "<xsd:element name=\"$element\" type=\"$type\">\n"
    }
    if {$documentation ne {}} {
        emit "<xsd:annotation>\n"
        emit "<xsd:documentation>$documentation</xsd:documentation>\n"
        emit "</xsd:annotation>\n"
    }
    emit "</xsd:element>\n"
}

proc emitSequence {sequence} {
    emit "<xsd:sequence>\n"
    foreach item $sequence {
        emitElement {} $item
    }
    emit "</xsd:sequence>\n"
}

proc emitTypeDef {type sequence documentation} {
    emit "<xsd:complexType name=\"$type\">\n"
    if {$documentation ne {}} {
        emit "<xsd:annotation>\n"
        emit "<xsd:documentation>$documentation</xsd:documentation>\n"
        emit "</xsd:annotation>\n"
    }
    emitSequence $sequence
    emit "</xsd:complexType>\n"
}

proc emit text {
    # This is used to emit the generated xsd text. Redefine this to match your usecase.
    error "You'll need to override emit"
}

proc beautify {xml} {
    set xml  "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n[[dom parse $xml] asXML -indent 2]"
}



# ================================

# redefine emit to do the right thing
proc emit {text} {
    append ::result $text
}

if {$argc < 1 || $argc > 2} {
    puts stderr "Usage: tsd2xsd tsd-file ?xsd-file?"
    exit
}

set path [pwd]

cd [file dirname [lindex $argv 1]] 
lassign $argv tsd xsd
source -encoding utf-8 $tsd

if {$xsd ne {} } {
    set f [open $xsd w]
    fconfigure $f -encoding utf-8
} else {
    set f stdout
}

if {[catch {puts $f [beautify $result]} error]} {
    puts stderr $errorInfo
    puts $f $result
}
close $f
cd $path