Soporte nativo del formato Tonel para Cuis Smalltalk: exportar, importar y versionar código en Git.
# cuis-tonel
Soporte nativo del formato Tonel para Cuis Smalltalk. Permite serializar clases desde la imagen a archivos `.class.st`, parsear esos archivos, importarlos de vuelta a la imagen, y exportar conjuntos enteros de clases a una estructura de directorios compatible con git.
---
## El problema que resuelve
El mecanismo tradicional de persistencia de Cuis son los *change sets* y los paquetes `.pck.st`. Ambos usan el formato *chunk*: un archivo de texto donde las definiciones de clases y métodos están separadas por el carácter `!`. El resultado es un archivo monolítico donde cualquier cambio mínimo —renombrar un método, agregar una línea— modifica el archivo completo.
En la práctica, esto hace que trabajar con git sea difícil:
- Un `git diff` de un cambio de una línea muestra cientos de líneas de contexto irrelevante.
- Es imposible ver en un PR qué método cambió y por qué.
- El historial por clase o por método no existe: solo hay historial del paquete completo.
- Los merges son propensos a conflictos innecesarios.
**Tonel resuelve todo esto.**
---
## El formato Tonel
Tonel es un formato de persistencia de código fuente para Smalltalk diseñado explícitamente para ser legible por humanos y amigable con git. Fue desarrollado por el equipo de Pharo y adoptado después por VAST, GemStone, Squeak y otras implementaciones.
Las reglas son simples:
- **Un archivo por clase**, con extensión `.class.st`.
- El archivo empieza con un bloque de metadatos en formato STON que describe el nombre de la clase, su superclase, las variables de instancia, las variables de clase, el paquete y el tag.
- Los métodos siguen inmediatamente: cada uno tiene una línea de categoría entre `{ }` y el cuerpo entre `[` `]`.
- Los archivos se organizan en directorios por paquete dentro de `src/`.
- Un archivo `.properties` en la raíz de `src/` marca el directorio como un repositorio Tonel.
### Ejemplo de un archivo `.class.st` real
Este es `src/Tonel/TonelWriter.class.st`, tal como lo genera este paquete:
```smalltalk
Class {
#name : 'TonelWriter',
#superclass : 'Object',
#category : 'Tonel',
#package : 'Tonel'
}
{ #category : 'writing' }
TonelWriter class >> sourceForClass: aClass [
^ String streamContents: [ :stream | self writeClass: aClass on: stream ]
]
{ #category : 'writing' }
TonelWriter class >> writeClass: aClass on: stream [
| comment category package tag classVars pools |
comment := aClass organization classComment.
comment isEmptyOrNil ifFalse: [
stream nextPut: $"; newLine; nextPutAll: comment; newLine; nextPut: $"; newLine ].
...
]
```
Comparado con el mismo contenido en formato chunk:
```
TonelWriter subclass: #TonelWriter instanceVariableNames: '' classVariableNames: '' ...!
!TonelWriter class methodsFor: 'writing'!
sourceForClass: aClass
^ String streamContents: [ :stream | self writeClass: aClass on: stream ]! !
!TonelWriter class methodsFor: 'writing'!
writeClass: aClass on: stream
...! !
```
La diferencia en un `git diff` es dramática: con Tonel, un cambio en `sourceForClass:` toca exactamente las líneas del método. Con chunk, todo el archivo cambia.
---
## Componentes
El paquete tiene seis clases:
| Clase | Responsabilidad |
|---|---|
| `TonelWriter` | Serializa una clase de la imagen al texto del formato Tonel |
| `TonelReader` | Parsea el contenido de un `.class.st` y devuelve una `TonelClassDefinition` |
| `TonelClassDefinition` | Objeto de valor que contiene los metadatos de una clase leída desde Tonel |
| `TonelMethodDefinition` | Objeto de valor que contiene el source y la categoría de un método |
| `TonelImporter` | Compila una `TonelClassDefinition` en la imagen (crea la clase y sus métodos) |
| `TonelImageExporter` | Exporta un conjunto de clases a una estructura de directorios Tonel |
---
## Instalación
### Desde la imagen (interactivo)
```smalltalk
CodePackageFile installPackage: '/ruta/a/cuis-tonel/dist/Tonel.pck.st' asFileEntry.
```
Para los tests también:
```smalltalk
CodePackageFile installPackage: '/ruta/a/cuis-tonel/dist/Tests-Tonel.pck.st' asFileEntry.
```
### Descarga directa
Cada push a `main` genera un release en GitHub con los archivos `.pck.st` adjuntos como assets. Se pueden descargar desde la sección [Releases](../../releases) del repositorio e instalar con el mismo `CodePackageFile installPackage:`.
---
## API completa con ejemplos
### `TonelWriter` — serializar una clase a texto
`TonelWriter` es stateless. Toda su API es de clase.
**Serializar una clase como texto Tonel:**
```smalltalk
| source |
source := TonelWriter sourceForClass: OrderedCollection.
Transcript show: source.
```
La salida incluye la definición STON completa y todos los métodos de instancia y de clase, ordenados alfabéticamente por selector:
```
Class {
#name : 'OrderedCollection',
#superclass : 'SequenceableCollection',
#instVars : [
'firstIndex',
'lastIndex'
],
#category : 'Collections-Sequenceable',
#package : 'Collections'
}
{ #category : 'adding' }
OrderedCollection >> add: newObject [
...
]
...
```
**Escribir directamente sobre un stream:**
```smalltalk
| stream |
stream := ReadWriteStream on: ''.
TonelWriter writeClass: OrderedCollection on: stream.
stream reset.
Transcript show: stream upToEnd.
```
**Obtener el nombre del paquete a partir de una categoría:**
```smalltalk
TonelWriter packageNameForCategory: 'Collections-Sequenceable'.
"=> 'Collections'"
TonelWriter packageNameForCategory: 'Tonel'.
"=> 'Tonel'"
```
Las categorías en Cuis siguen la convención `Paquete-Tag`. El writer separa en el primer guion para determinar el directorio de destino.
---
### `TonelReader` — parsear un archivo `.class.st`
`TonelReader` lee un stream y devuelve una `TonelClassDefinition`.
**Parsear desde un archivo:**
```smalltalk
| entry definition |
entry := '/ruta/a/src/Tonel/TonelWriter.class.st' asFileEntry.
entry readStreamDo: [ :stream |
definition := TonelReader readStream: stream ].
definition className. "=> #TonelWriter"
definition superclassName. "=> #Object"
definition instanceVariableNames. "=> #()"
definition classVariableNames. "=> #()"
definition category. "=> #'Tonel'"
definition package. "=> #'Tonel'"
definition methods size. "=> 8"
```
**Inspeccionar los métodos leídos:**
```smalltalk
definition methods do: [ :method |
Transcript
show: method selector;
show: ' (';
show: (method classSide ifTrue: ['clase'] ifFalse: ['instancia']);
show: ') — categoría: ';
show: method protocol;
newLine ].
```
**Parsear desde un string en memoria:**
```smalltalk
| source definition |
source := 'Class {
#name : ''MiClase'',
#superclass : ''Object'',
#category : ''MiPaquete'',
#package : ''MiPaquete''
}
{ #category : ''accessing'' }
MiClase >> valor [
^ 42
]'.
definition := TonelReader readStream: source readStream.
definition className. "=> #MiClase"
definition methods first selector. "=> #valor"
```
---
### `TonelClassDefinition` y `TonelMethodDefinition` — objetos de valor
`TonelClassDefinition` es lo que devuelve el reader. Contiene todo lo necesario para reconstruir la clase en la imagen.
```smalltalk
| definition |
"... (parsear como en el ejemplo anterior)"
definition className. "=> #TonelWriter"
definition superclassName. "=> #Object"
definition instanceVariableNames. "=> #()"
definition classVariableNames. "=> #()"
definition classInstanceVariableNames. "=> #()"
definition poolDictionaries. "=> #()"
definition category. "=> #'Tonel'"
definition package. "=> #'Tonel'"
definition tag. "=> nil (o #'Tonel' si hay tag)"
definition comment. "=> '' (o el texto del comentario de clase)"
definition methods. "=> OrderedCollection de TonelMethodDefinition"
```
Cada elemento de `definition methods` es una `TonelMethodDefinition`:
```smalltalk
| method |
method := definition methods first.
method selector. "=> #sourceForClass:"
method classSide. "=> true"
method protocol. "=> #'writing'"
method source. "=> 'sourceForClass: aClass\n\t^ String streamContents: ...'"
method className. "=> #TonelWriter"
```
Estos objetos son inmutables en uso normal. Se crean solo desde el reader o en tests.
---
### `TonelImporter` — importar una definición a la imagen
`TonelImporter` toma una `TonelClassDefinition` y la compila en la imagen.
**Importar un archivo directamente:**
```smalltalk
TonelImporter importFile: '/ruta/a/MiClase.class.st' asFileEntry.
```
**Importar desde un stream:**
```smalltalk
'/ruta/a/MiClase.class.st' asFileEntry readStreamDo: [ :stream |
TonelImporter importStream: stream ].
```
**Importar una definición ya parseada:**
```smalltalk
| definition |
definition := TonelReader readStream: miStream.
TonelImporter importDefinition: definition.
```
**Importar todo un directorio de archivos Tonel:**
```smalltalk
TonelImporter importDirectory: '/ruta/a/src/MiPaquete' asDirectoryEntry.
```
Esto recorre el directorio recursivamente e importa todos los archivos `.st` que encuentre.
**Reimportar (actualizar una clase existente):**
El importer no recrea la clase si ya existe: detecta si `Smalltalk classNamed: className` devuelve algo. Si la clase existe, solo recompila los métodos. Esto permite actualizar el código de una clase en ejecución sin perder el estado de la imagen.
```smalltalk
"Primera vez: crea la clase"
TonelImporter importFile: 'MiClase.class.st' asFileEntry.
"Después de modificar el archivo: actualiza los métodos"
TonelImporter importFile: 'MiClase.class.st' asFileEntry.
```
**Limitación actual**: si se eliminaron métodos del archivo `.class.st`, el importer no los borra de la imagen. Solo agrega y sobreescribe. Ver sección [Lo que falta](#lo-que-falta).
---
### `TonelImageExporter` — exportar clases desde la imagen
`TonelImageExporte