cuis-tonel

Description

Soporte nativo del formato Tonel para Cuis Smalltalk: exportar, importar y versionar código en Git.

Details

Source
GitHub
Dialect
cuis (25% confidence)
License
MIT
Created
May 22, 2026
Updated
May 29, 2026

Categories

Packaging / VCS

README excerpt

# 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
← Back to results