domingo, 18 de octubre de 2009

Parser para un simple DSL

Dado que estoy empezando a usar un DSL, me gustaría compartir mi experiencia, por si otros deciden incursionar en el mundo de los DSL. Voy a aprovechar para contar que me lleva a usar un DSL.

Uno de los problemas de mi dominio es resolver el binding de los filtros, pertenecientes a mi domain model, en una consulta MDX. Para eso adopté la siguiente sintaxis:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE (${category}, ${product}, ${year})

donde si category=A, product=B, y year no tiene valor, la query debería valer:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE (A, B)

MDX es un DSL usado para resolver consultas multidimensionales. Lo que estoy haciendo es embebiendo mi DSL adentro de otro DSL (por ahora es tan simple, que no merece llamarse DSL).

El primer paso para usar mi DSL es parsear los ${...} para extraer la expresión del mismo. Seguramente hay muchas formas de parsear los ${...}. Yo decidí usar un tokenizer basado en palabras, que devuelve el texto hasta encontrar el token buscado. Esta es la interface de WordTokenizer:

la implementación basada en la clase String:

y las pruebas unitarias (aguante TDD):

Es simple extraer es las expresiones de mi DSL usando el word tokenizer:

Con esto extraigo mi DSL de la consulta MDX. Este por ahora es simple, pero hay casos que tienen un poco más de complejidad.

Por ejemplo, si un filtro no tiene valor, tengo que aplicar el valor default:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE ${year.default([Time].[Year].[2009])}

O distintos filtros que forman una jerarquia (mes, trimestre, año) y solo debe reemplazarse un valor en la consulta:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE ${month.parent(quater).parent(year)}

O una combinación de jerarquías y valores default. Para resolver estos y otros casos, usé lo que se llama fluent interface, que permite fácilmente armar DSL para un problema de dominio (es fácil de leer y simple de programar).

Para parsearlo uso un simple StringTokenizer:

La implementación es bastante simple. Seguramente hay casos que no se cubren, pero sobre esta base es fácil agregar nuevos métodos al DSL.

Hay otros conceptos interesantes asociados a un DSL, como:

saludos!

No hay comentarios.: