DRY package description

Tagged:

If you are used to having one class per file, package descriptions tend to get a bit unwieldy. Take a look at Iliad's Core/package.xml, as an example. What you see is a lot of typing, some of it, gasp, even repeated. Let's DRY this up a bit.

Wrapping in tags

We're going to wrap "short" (one-liners) and "long" (opening and closing tags on separate lines)

content:
 FileStream extend [ 
 
   tag: aString [ 
     self nextPut: $<; nextPutAll: aString; nextPut: $>
   ]
 
   wrap: aString do: aBlock [
     self tag: aString; nl.
     aBlock value.
     self tag: '/' , aString; nl.
   ]
 
   wrap: aString around: anotherString [ 
     self tag: aString; nextPutAll: anotherString; tag: '/' , aString; nl
   ]
 ]

Parsing JSON

Since the itch I'm rubbing here is closely connected with Iliad, I let myself get talked into starting with a JSON-based package description, which I distilled from the original package.xml.
Some general package related properties are followed by a compact description of where to find test files and the ordered sequence of fileins, from which the corresponding <file> tags can be deduced.

 {
   "package": {
     "name": "Iliad-Core",
     "namespace": "Iliad",
     "prereq": [ "Sport", "Iconv" ]
   },
   "test": {
     "root": "Tests/Unit",
     "extension": ".st",
     "pattern": "subclass: *Test"
   },
   "filein": [
     "Utilities/IliadObject.st",
     "Utilities/Support.st",
     ... cut for the sake of brevity
     "RequestHandlers/JsonHandler.st",
     "RequestHandlers/ApplicationHandler.st",
     "RequestHandlers/RedirectHandler.st"
   ]
 }

To get an accessible dictionary out of this, you just need to do something like this:

 Eval [ |stream data|
   PackageLoader fileInPackage: 'Iliad-Core'.  
   stream := FileStream open: 'package.json' mode: FileStream read.
   data := Iliad.Json readFrom: stream.
   stream close.
 ]

You might need to squash a buglet in Core/lib/JSON/Json.st (replace JsonObject with Dictionary).

Creating XML

Now that we have our data and the wrapping, generating the XML is straightforward:

 Eval [
   |stream data package test filein|
   stream := FileStream open: 'package.json' mode: FileStream read.
   data := Iliad.Json readFrom: stream.
   stream close.
   package := data at: 'package'.
   test := data at: 'test'.
   filein := data at: 'filein'.
   [ :out |
     out wrap: 'package' do: [ 
       out wrap: 'name' around: ( package at: 'name' ).
       ( package at: 'prereq' ) do: [ :p | out wrap: 'prereq' around: p ].
       out wrap: 'namespace' around: ( package at: 'namespace' ).
       out wrap: 'test' do: [ out nextPutAll: 'TODO'; nl ].
       filein do: [ :f | out wrap: 'filein' around: f ].
       filein do: [ :f | out wrap: 'file' around: f ]
     ]
   ] value: FileStream stdout.
 ]

There are some things left to do, but in its current state it already does get rid of a lot of duplication.

Some fancy indenting, not really elegant, but nice to look at.
Additionally, all matching files below the test root are included.
Bug: filein sequence for test files not under user control.
Solution: do what smart people say and rewrite the package spec in smalltalk code.

Eval [
  PackageLoader fileInPackage: 'Iliad-Core'
]

FileStream extend [ 
  |indent|

  indentation [ ^ indent ifNil: [ indent := '' ] ]
  indent [ indent := indent , '  ' ]
  outdent [ 
    ( indent size < 2 )
      ifTrue: [ indent := '' ]
      ifFalse: [ indent := indent copyFrom: 1 to: indent size - 2 ]
  ]

  tag: aString [ 
    self nextPut: $<; nextPutAll: aString; nextPut: $>
  ]

  wrap: aString do: aBlock [
    self nextPutAll: self indentation; tag: aString; nl.
    self indent.
    aBlock value.
    self outdent.
    self nextPutAll: self indentation; tag: '/',aString; nl.
  ]

  wrap: aString around: anotherString [ 
    self 
      nextPutAll: self indentation;
      tag: aString; 
      nextPutAll: anotherString; 
      tag: '/',aString; 
      nl
  ]
]

Eval [
  |stream data package test filein|
  stream := FileStream open: 'package.json' mode: FileStream read.
  data := Iliad.Json readFrom: stream.
  stream close.
  package := data at: 'package'.
  test := data at: 'test'.
  filein := data at: 'filein'.
  [ :out |
    out wrap: 'package' do: [ 
      out wrap: 'name' around: ( package at: 'name' ).
      ( package at: 'prereq' ) do: [ :p | out wrap: 'prereq' around: p ].
      out wrap: 'namespace' around: ( package at: 'namespace' ).
      out wrap: 'test' do: [ |testRoot printer|
        testRoot := File name: ( test at: 'root' ).
        printer := [ :tag |
          testRoot allFilesMatching: '*.st' do: [ :f|
            out wrap: tag around: ( testRoot pathTo: f ).
          ].
        ].
        printer value: 'filein'; value: 'file'.
      ].
      filein do: [ :f | out wrap: 'filein' around: f ].
      filein do: [ :f | out wrap: 'file' around: f ]
    ]
  ] value: FileStream stdout.
]

Indeed, I hope in the future there will be a GUI to create packages. GNU Smalltalk has never been so close to that, so...

User login