DRY package description
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...