Download Transformaciones de modelos
Document related concepts
no text concepts found
Transcript
Máster MNTI Desarrollo Dirigido por Modelos Seminario 3 - Transformaciones de modelos Grupo Modelum Universidad de Murcia 15 de diciembre de 2010 Resumen En este seminario se presentarán los conceptos relativos a la transformación de modelos. El objetivo final es aprender a utilizar el lenguaje de transformación RubyTL, para poder transformar modelos de sintaxis abstracta del DSL. Primero se explicará RubyTL a través de una serie de ejemplos sencillos. También, utilizando RubyTL, se introducirá una técnica para comprobar la semántica estática (restricciones) asociada los metamodelos. 1 1. Introducción En este seminario se presentarán los conceptos relativos a la transformación de modelos. Se utilizará el lenguaje de transformación RubyTL para practicar con los conceptos. Se introducirá RubyTL utilizando un ejemplo simple de transformación de un modelo de clases a un modelo Java y se describirán los ejercicios del caso práctico. Antes de seguir completando el DSL desarrollado hasta ahora se realizará un pequeño ejercicio práctico de transformación de modelos para aprender algunos conceptos básicos con un ejemplo más sencillo. El enunciado del ejercicio se encuentra en el Anexo D. 2. Breve introducción a RubyTL RubyTL es un lenguaje de transformación modelo-modelo que ha sido implementado como un lenguaje embebido dentro de Ruby. Esto significa que no se ha utilizado la técnica habitual de implementar un parser y un interprete o un compilador, sino que la sintaxis de RubyTL está basado en las construcciones ofrecidas por el propio Ruby, en este caso, llamadas a métodos. La implicación fundamental de esta técnica de implementación es que es posible utilizar código Ruby imperativo en cualquier parte de una definición de transformación1 . RubyTL es un lenguaje de transformación basado en reglas hı́brido. La parte declarativa está basada en reglas y bindings, mientras que la parte imperativa viene dada por el propio Ruby. En RubyTL una definición de transformación está compuesta de reglas de transformación. Como se verá, hay varios tipos de reglas de transformación, pero todas ellas comparten la misma estructura. Están compuestas de cuatro partes from, to, filter and mapping: from. Especifica una metaclase origen, que pertenece a un metamodelo origen. La regla se aplicará sobre elementos del modelo origen que sean de ese tipo o de alguna de sus subclases. to. Especifica una o más metaclases destino, que pertenecen a un metamodelo destino. La regla creará elementos de ese tipo (excepto si es una regla de refinamiento). filter. Una expresión sobre el elemento origen. La regla solo se ejecuta si esta expresión (un bloque de código, en realidad) se evalúa a true. Esta parte es opcional. mapping. Un bloque de código de Ruby, que se ejecuta cuando se aplica la regla. El bloque recibe un elemento origen y el elemento destino creado por la regla. Dentro del bloque es posible escribir cualquier código Ruby, aunque se recomienda un estilo declarativo basado en bindings. Un binding es un tipo especial de asignación que tiene la siguiente estructura expr izq.propiedad = Si el tipo de la expresión de la parte derecha no es compatible con el tipo de la propiedad especificada, el motor de RubyTL automáticamente busca una regla capaz de transformar la parte derecha en la parte izquierda. Hay tres tipos básicos de reglas: expr der. Regla “top”. Sirve como “regla main” para iniciar la ejecución. Se aplica sobre todos los elementos del modelo origen del tipo que indique su parte from. Regla “normal”. Se ejecuta solo como resultado de evaluar un binding. Una caracterı́stica peculiar (que comparte con las reglas “top”) es que nunca transforma dos veces el mismo elemento. Si esto sucede, se devuelve el elemento destino creado previamente. Regla “copy”. Se ejecuta solo como resultado de evaluar un binding, pero en este caso un elemento puede ser transformado más de una vez. La ejecución de una transformación empieza con la ejecución de las reglas “top”. Para cada regla top (normalmente habrá una por cada transformación) se obtienen todos elementos del modelo origen cuyo tipo es la clase especificada en la parte from o una de sus subclases. Para cada elemento origen se crea el correspondiente elemento destino. Si no existen reglas top se considera la primera regla de la transformación como tal. A continuación se ejecuta la parte mapping, ejecutando los bindings que se hayan especificado. La ejecución de los bindings implicará la ejecución de ciertas reglas, para las cuales se ejecutará también los bindings correspondientes. 1 La primera edición del libro Programming Ruby está disponible en la web: http://www.rubycentral.com/book/. 2 2.1. Ejemplo: UML2Java El código RubyTL que se muestra en esta sección corresponde a una definición transformación que transforma modelos conformes al metamodelo de la Figura 1a, en modelos conformes al metamodelo de la Figura 1b. (a) Metamodelo de clases (b) Metamodel Java Figura 1: (a) Metamodelo de clases simple. (b) Metamodelo Java. Las correspondencias que se establecen son las siguientes: UML Clase Atributo Atributo Operación Parámetro Tipo primitivo Java Clase, Interface Método get, Método set Variable de instancia Método Parámetro Tipo primitivo Comentario La clase implementa la interfaz Solo atributos públicos Todos Para clases e interfaces Además se desea que las clases e interfaces Java que se generen queden englobadas en un paquete Java, que no tiene ninguna correspondencia con ningún elemento del modelo UML. Igual ocurre con los tipos primitivos, que deben quedar contenidos en un paquete llamado java.lang. El código de transformación es el siguiente. decorator UML::Class do def attributes self.features.select { |f| f.kind_of?(UML::Attribute) } end def operations self.features.select { |f| f.kind_of?(UML::Operation) } end end lang_pkg = Java::Package.new(:name => ’java.lang’) app_pkg = Java::Package.new(:name => ’mnti2010’) top_rule ’class2class’ do from UML::Class to Java::Class, Java::Interface mapping do |uml_class, java_class, java_interface| java_class.name = uml_class.name java_class.features = uml_class.attributes java_class.implements = java_interface java_class.features = uml_class.operations java_interface.name = ’I’ + uml_class.name java_interface.abstractMethods = uml_class.operations. select { |o| o.visibility == UML::Visibility::PUBLIC } app_pkg.classifiers << java_class app_pkg.classifiers << java_interface end end rule ’attribute2get_set’ do from UML::Attribute to Java::Field mapping do |attr, field| field.name = attr.name field.type = attr.type field.visibility = ’private’ end 3 end rule ’attribute2get_set’ do from UML::Attribute to Java::Method, Java::Method filter { |attr| attr.visibility == UML::Visibility::PUBLIC } mapping do |attr, get, set| get.name = ’get’ + attr.name.capitalize get.type = attr.type get.visibility = ’public’ set.name = ’set’ + attr.name.capitalize set.visibility = ’public’ set.params = Java::Parameter.new do |param| param.name = attr.name param.type = attr.type end end end copy_rule ’operation2method’ do from UML::Operation to Java::Method mapping do |operation, method| method.name = operation.name method.type = operation.type method.params = operation.params end end copy_rule ’type2parameter’ do from UML::Parameter to Java::Parameter mapping do |uml_param, java_param| java_param.name = uml_param.name java_param.type = uml_param.type end end rule ’datatype2primitive’ do from UML::PrimitiveType to Java::PrimitiveType mapping do |src, target| target.name = src.name target.owner = lang_pkg end end 2.2. Ejecución de transformaciones en RubyTL Para ejecutar una transformación hay que especificar cuáles son los modelos y metamodelos de entrada concreto que usa la definición de transformación. En el caso de RubyTL, se definen ficheros Rakefile (un sistema parecido a Make, pero embebido en Ruby) en los que se especifican tareas de construcción. Ası́, hay tareas especı́ficas para transformaciones modelo-modelo, modelo-código, etc. A continuación se muestra un ejemplo de definición de una tarea para ejecutar la transformación de la sección anterior. model_to_mo del : uml2java do | t | t . sources : package = > ’ UML ’ , : metamodel = > ’ metamodels / SUML . ecore ’ , : model = > ’ models / test . class . xmi ’ t . targets : package = > ’ Java ’ , : metamodel = > ’ metamodels / JavaM . ecore ’ , : model = > ’ models / result . xmi ’ t . transfor mation ’ tr an s fo rm at i on s / uml2java . rb ’ end Una tarea model to model está compuesta de los siguiente elementos. El nombre de la tarea. Por convención se prefija con “:”, aunque puede especificarse como una cadena. En este caso :uml2java. Un conjunto de elementos sources, que representan modelos y metamodelos de entrada. Se especifica el espacio de nombres que espera la definición transformación (ej., UML), el fichero .ecore que contiene el metamodelo, y un modelo que conforma al metamodelo. 4 Un conjunto de elementos targets, que representan modelos y metamodelos de salida. Se especifica el espacio de nombres que espera la definición transformación (ej., Java), el fichero .ecore que contiene el metamodelo, y un modelo que conforma al metamodelo. Si el modelo no existe, al finalizar la transformación se creará nuevo, si ya existe será reemplazado por el nuevo. La ruta de la transformación que se ejecutará. 2.2.1. Resumen de comandos Click der. (carpeta) → New → Other ... → AGE → New RubyTL Transformation Crear nueva transformación RubyTL Click der. (proyecto) → New File → Nombrar acabado en .rake Crear nuevo fichero Rakefile Escribir mo → CTRL + Espacio Autocompletado de tareas Rake 2.3. Librerı́a colecciones Una parte importante de la tarea de escribir una transformación de modelos se suele dedicar a escribir expresiones de navegación sobre el modelo origen. La navegación sobre referencias multivaluadas se reduce a un problema de manipulación de colecciones. Ruby tiene una potente librerı́a para manipular colecciones (arrays según su terminologı́a), que está basada en el uso de closures o bloques de código para implementar iteradores. Estos iteradores tienen un estilo “funcional”, parecido a las operaciones sobre colecciones de OCL. Se puede encontrar documentación sobre la librerı́a de colecciones en la documentación de las clases Enumerable y Array, que se puede consultar utilizando el comando ri de la distribución Ruby o en http://www.ruby-doc.org/. A continuación se explica a través de ejemplos, los métodos más importantes. Estos métodos no tienen efectos laterales, es decir, siempre devuelven un nuevo array. En Ruby las colecciones está implementadas en la clase Array. Son arrays dinámicos que no tiene un tamaño prefijado. Hay varias formas de crear arrays: # Crear un array con tres elementos [1, 2, 3] # Crear un array con tres elementos, otra forma mi_array = [] mi_array << 1 mi_array << 2 mi_array << 3 Para filtrar elementos de una lista, se utiliza select y reject. [1, 2, 3, 4].select { |i| i % 2 == 0 } # => [2, 4] [1, 2, 3, 4].reject { |i| i % 2 == 0 } # => [1, 3] Transformaciones “in-place” con el método map. Cada elemento del array se sustituye por el resultado del bloque. list = [1, 2, 3, 4] list.map { |i| i.to_s } # => ["1", "2", "3", "4"] list.map { |i| i * 2 } # => [2, 4, 6, 8] Para aplanar una colección, se utiliza flatten [[1, 2], [3, 4], [5, 6]].flatten # => [1, 2, 3, 4, 5, 6] [1, 3, 5].map { |i| [i, i + 1] }.flatten # => [1, 2, 3, 4, 5, 6] Para buscar el primer elemento que cumple una condición, se utiliza find 5 [1, 2, 3, 4].find { |i| i > 2 } # => 3 [1, 2, 3, 4].find { |i| i > 4 } # => nil Para comprobar si un elemento está incluido en un array, se utiliza include?. [1, 2, 3, 4].include?(3) # => true [1, 2, 3, 4].include?(5) # => false Para eliminar duplicados, se utiliza uniq [1, 2, 2, 4, 1].uniq # => [1, 2, 4] Para iterar sobre una colección y realizar una acción en cada paso de la iteración se utiliza each. Por ejemplo, [1, 2, 3, 4, 5].each do |value| puts "El numero actual es #{value}" end 2.4. new Crear elementos imperativamente RubyTL reutiliza la manera de crear instancias que se utiliza en Ruby: se aplica el método de clase a la clase que se quiere instanciar. Por ejemplo, my_pkg = Java::Package.new my_pkg.name = ’java.lang’ RubyTL proporciona dos atajos para establecer los valores de las propiedades de un objeto creado imperativamente directamente: my_pkg2 = Java::Package.new(:name => ’java.lang’) my_pkg3 = Java::Package.new do |p| p.name = ’java.lang’ end 3. Validación de la semántica estática Esta sección está basada en el artı́culo “Using ATL for checking models” 2 en el cual se presenta una aproximación para la comprobación de restricciones en modelos utilizando el lenguaje ATL. Para información más detallada consultar el artı́culo. Existen actualmente pocas herramientas que permitan asociar restricciones a un metamodelo y comprobar automáticamente que tales restricciones se cumplen para los modelos conformes al metamodelo. En ocasiones es necesario asegurar que se cumplen restricciones adicionales “dependientes del contexto”. Estas restricciones adicionales no estarı́an asociadas al metamodelo, sino que deben escribirse en algún fichero separado. Otra motivación para esta aproximación, en lugar de un procesador OCL, es la necesidad de asociar grados de “severidad” a las restricciones. En el artı́culo citado anteriormente se consideran tres niveles: error, advertencia y sugerencia (error, warning y critic). Además, también permite asociar descripciones a las restricciones. Por ejemplo, a continuación se muestran dos restricciones en OCL. La primera comprueba un error que debe ser resuelto e impide la manipulación del modelo (p.ej., aplicar una transformación de modelos) hasta entonces. La segunda en cambio es una advertencia. -- Error: No se permiten referencias multivaluadas para atributos context UML::Attribute inv: self.type.oclIsKindOf(UML::Primitive) implies self.multivalued = false -- Warning: No haya nombres repetidos en los atributos de una clase context UML::Class inv: self.features->forAll(f1 | self.features->excluding(f1)->forAll(f2 | f1.name <> f2.name)) 2 Puedes descargarse de: www.sciences.univ-nantes.fr/lina/atl/www/papers/GraMoT05.pdf 6 Utilizando una transformación de modelos es posible comprobar restricciones. El modelo origen será aquel que se desea “chequear” mientras que el modelo destino conformará al metamodelo Problem presentado en la Figura 2. Como se observa es un metamodelo muy simple con una sola metaclase. El resultado de la ejecución de la transformación será un modelo que contendrá una instancia de la metaclase Problem por cada restricción no satisfecha. Si el modelo destino está vacio, significará que todas las restricciones son satisfechas por el modelo origen. Figura 2: Metamodelo Problem Ası́, una definición de transformación en RubyTL para la comprobación de restricciones sigue el siguiente esquema. Una regla “top” por cada restricción. La metaclase origen de cada regla es el contexto de la restricción. El filtro o guarda de la regla contiene la expresión booleana asociada, pero negada. Es decir, seguirá el patrón not ( condition ). La metaclase destino de la regla siempre es Problem, de manera que por cada instancia que cumpla el filtro se creará una instancia de Problem (es decir, por cada violación del invariante. Notese la negación en el filtro). Las propiedades Problem se inicializan utilizando dos bindings, la “severidad” y una descripción. La dos restricciones OCL anteriores se pueden implementar en RubyTL de la siguiente forma. top_rule ’InvalidMultivalued’ do from UML::Attribute to Problem::Problem filter { |a| ! (a.type.kind_of?(UML::PrimitiveType).implies(!a.multivalued) ) } mapping do |attr, problem| problem.description = "Atributo #{attr.name} es multivaluado" problem.severity = Problem::Severity::ERROR end end top_rule ’DuplicatedFeatureName’ do from UML::Class to Problem::Problem filter { |c| ! (c.features.all? { |f1| c.features.reject { |f2| f2 == f1 }.all? { |f2| f2.name != f1.name } } ) } mapping do |klass, problem| problem.description = "Nombre de caracteristica duplicada" problem.severity = Problem::Severity::WARNING end end 7 4. Caso práctico 4.1. Representación de “bundles” En el DSL que se está desarrollando durante el curso se debe tratar el aspecto de los ficheros de traducción y su utilización en una aplicación. Básicamente manejar las traducciones de una aplicación consiste en utilizar nombres lógicos (claves) para nombrar los mensajes que utiliza la aplicación. En un fichero de traducción, denominado bundle, se asocian los nombres lógicos o claves con el texto del mensaje. Por ejemplo, el siguiente trozo de código muestra el contenido de un bundle, denominado MensajesEjemplo es ES.properties. menuId . showedName = File menuId . shortcut = ctrl + f menuId . mnemonic = f Un posible código Java para “internacionalizar” una aplicación es el siguiente. Ası́, en lugar de utilizar literales de cadena se utilizan los métodos del objeto ResourceBundle que accede al fichero de traducción adecuado para obtener la versión adecuada, según el “locale” seleccionado. Locale currentLocale = esLocale = new Locale ( " es " ," ES " ) ; ResourceBund le messages = Resourc eBundle . getBundle ( " Me ns a je sE j em pl o " , currentLocale ) ; // Escribir una cadena " i n t e r n a c i o n a l i z a d a " por pantalla System . out . println ( messages . getString ( " saludo " ) ) ; Para facilitar la generación de los bundles, es posible crear un metamodelo que los represente, de manera que se pueden crear transformaciones modelo-modelo para reutilizar el generador de código para diversos DSLs. 4.2. Representación de la arquitectura Uno de los posibles objeticos de un DSL es permitir la generación automática de código fuente asociado a una arquitectura. Para facilitar esta labor, muchas veces se hace necesario reducir el nivel de abstracción de los modelos con los que trabajamos con el DSL (sintaxis abstracta) para ser más cercanos al dominio de aplicación. Para ello, se definen metamodelos de la arquitectura destino y son necesarias transformaciones de modelos para convertir modelos de la sintaxis abstracta en modelos de la arquitectura. Finalmente, los modelos de la arquitectura, por ser cercanos al dominio de aplicación, facilitan la generación de código por medio de transformaciones modelo a código. En el caso del DSL que se está desarrollando durante el curso, queremos permitir la generación automática del código asociado a los menús y los perfiles. Nuestro objetivo será generar el código Java para el framework Swing que permite generar los menús y perfiles de una aplicación. De esta forma, hemos definido el metamodelo de la arquitectura mostrado en el Anexo B. 4.3. Ejercicios Para realizar estos ejercicios, descarga el proyecto esqueleto que hay en la web de la asignatura. Ejercicio 1. Identificar las correspondencias entre el metamodelo de la sintaxis abstracta del DSL y el metamodelo de “bundles” (diagrama del metamodelo en A. Completar la transformación menuprofile2bundle.rb. Ejercicio 2. Para comprobar la semántica estática del metamodelo de la sintaxis abstracta se escribirá la definición de transformación dsl2problem.rb y deberá seguir la aproximación presentada en la Sección 3. Se deberán comprobar las siguientes restricciones para el DSL de los menús (entre corchetes se indica el grado de problema a indicar): • Dos menús no pueden compartir nombre (atributo id [ERROR]. • MenuDefinition contiene al menos un TreeMenu [WARNING] • MergeMenu debe unir al menos dos menús [ERROR] • Todos los menús deben estar internacionalizados para cada idioma [ERROR]. 8 Ejercicio 3. Se creará una transformación de la sintaxis abstracta del DSL a un metamodelo que representa la arquitectura de los menús. Ambos metamodelos se pueden consultar en el anexo B. En particular, el metamodelo de la arquitectura trata de representar un “gestor de menús” con la estructura que se muestra en el trozo de código Java incluido en el anexo C. Implementar la transformación menuprofile2arch.rb sin considerar la herencia de perfiles ni los enlaces de menús. Se enviará el proyecto Eclipse con los ejercicios resueltos a jlcanovas@um.es. Opcionalmente se puede implementar la transformación considerando la herencia de perfiles y enlaces de menús. 9 A. Metamodelo de internacionalización Figura 3: Metamodelo para representar ficheros de internacionalización. 10 B. Metamodelos para la transformación dsl2arch 11 C. Código Java destino package imaci09 ; import java . awt . event . ActionEvent ; import java . awt . event . KeyEvent ; import import import import import javax . swing . J C h e c k B o x M e n u I t e m ; javax . swing . JMenu ; javax . swing . JMenuBar ; javax . swing . JMenuItem ; javax . swing . KeyStroke ; class MenuManager { private JMenu File ; private JMenuItem New ; private JMenuItem Close ; private J Ch e c k B o x M e n u I t e m Synchronize ; private JMenuBar N o v i c e U s e r P r o f i l e M e n u ; private JMenuBar A d v a n c e d U s e r P r o f i l e M e n u ; public final static MenuManager INSTANCE = new MenuManager () ; private MenuManager () { createMenus () ; ... } public void createMenus () { this . File = new JMenu ( " Fichero " ) ; this . File . s etToolTi pText ( " Menu Fichero " ) ; this . File . setMnemonic ( KeyEvent . VK_F ) ; ... this . New = new JMenuItem ( " New " ) ; ... this . Edit = new JMenuItem ( " Edit " ) ; ... this . N o v i c e U s e r P r o f i l e M e n u = new JMenuBar () ; this . A d v a n c e d U s e r P r o f i l e M e n u = new JMenuBar () ; } public JMenuBar g e t P r o f i l e N o v i c e U s e r () { this . N o v i c e U s e r P r o f i l e M e n u . add ( this . File ) ; this . File . add ( this . New ) ; ... return N o v i c e U s e r P r o f i l e M e n u ; } public JMenuBar g e t P r o f i l e A d v a n c e d U s e r () { this . A d v a n c e d U s e r P r o f i l e M e n u . add ( this . File ) ; this . File . add ( this . New ) ; ... this . File . add ( this . Edit ) ; ... return A d v a n c e d U s e r P r o f i l e M e n u ; } } 12 D. Ejercicio de transformación: árboles de sintaxis concreta Este ejercicio consiste en transformar un árbol de sintaxis concreta en un árbol de sintaxis abstracta de cierto lenguaje. Supongase que un parser reconoce un lenguaje conforme a cierta gramática en BNF. El resultado de la ejecución del parser podrı́a ser un modelo de acuerdo a un metamodelo para representar árboles de sintaxis concreta. Para crear la sintaxis abstracta del lenguaje, se debe definir una transformación de modelos. Ası́, dada una gramática que reconoce un lenguaje y genera un modelo del árbol de sintaxis concreta que contiene el resultado de reconocer el lenguaje, con una transformación modelo-modelo se crea un modelo de la sintaxis abstracta de dicho lenguaje. Se utilizará el metamodelo mostrado en 4a para representar el árbol de sintaxis concreta de cualquier lenguaje reconocido por el parser “imaginario” de este ejercicio. Un posible lenguaje serı́a el que se muestra a continuación, llamado “struct”, y que sirve para representar estructuras de datos: struct Person { fullname : String; address : String; age : Integer; } El modelo de sintaxis concreta que se generarı́a al reconocer el código anterior serı́a el que se muestra en la Figura 4b. Un objeto de tipo Leaf corresponde a un token (o sı́mbolo terminal) de la gramática usada para reconocer el lenguaje. Un objeto Node corresponde a la aplicación de una regla gramátical (un no-terminal), que probablemente tendrá uno o más hijos (bien nodos u hojas). (a) Metamodelo de sintaxis concreta (b) Modelo de ejemplo (c) Metamodelo del lenguaje “struct” Figura 4: (a) Metamodelo para representar el árbol de sintaxis concreta que puede reconocer un parser. (b) Posible modelo para un código del lenguaje “struct”. (c) Metamodelo lenguaje “struct” Se asume que, para el lenguaje “struct”, cualquier programa válido satisface las restricciones que se muestran a continuación (la palabra clave context indica que los invariantes que van entre do - end se aplican a la metaclase especificada, mientras que inv indica un invariante, y el código entre do end es la expresión que evalua el invariante). context CST :: Node do inv ’ struct ’ do ( self . kind == ’ struct ’) . implies ( self . children . select { | n | n . kind_of ? CST :: Leaf }. size == 2 && 13 self . children . select { | n | n . kind_of ? CST :: Leaf }. any ? { | n | n . kind == ’ name ’ } ) end inv ’ body ’ do ( self . kind == ’ body ’) . implies ( self . children . all ? { | n | n . kind_of ? CST :: Node } && self . children . all ? { | n | n . kind == ’ attribute ’ } ) end inv ’ attribute ’ do ( self . kind == ’ attribute ’) . implies ( self . children . size == 2 && self . children . all ? { | n | n . kind_of ? CST :: Leaf } && self . children . any ? { | n | n . kind == ’ name ’ } && self . children . any ? { | n | n . kind == ’ type ’ } ) end end context CST :: Leaf do inv ’ ptype ’ do ( self . kind == ’ type ’) . implies ( [ ’ String ’ , ’ Integer ’ , ’ Any ’ ]. include ?( self . value ) ) end end La Figura 4c muestra el metamodelo del lenguaje “struct”. La metaclase Data agrega una o más definiciones de estructuras. Un struct está compuesta de atributos. Un attribute tiene un nombre y un tipo, que obligatoriamente tiene que ser uno de los siguientes “String”, “Integer” o “Any” (se puede ver como un puntero a otra estructura). D.1. Tarea Escribir una transformación modelo-modelo con RubyTL que crea un modelo de la sintaxis abstracta para el lenguaje “struct”. D.2. Ayuda Los siguientes apuntes pueden servir de ayuda para escribir la transformación requerida en el ejercicio. Decoradores. Los decoradores permiten añadir métodos a una metaclase en tiempo de ejecución. Esto es útil para factorizar código de navegación. Por ejemplo, el siguien decorador añade un método a la metaclase Node para devolver todos los sus hijos que sean de tipo Node (esto es, no considera los objetos de tipo Leaf). decorator CST :: Node do def nodes self . children . select { | n | n . kind_of ?( CST :: Node ) } end end Enumerados. Para acceder a un valor enumerado, se debe usar la sintaxis ::: Por ejemplo, en este ejercicio se necesita acceder a los Paquete::TipoEnumerado::NombreDelLiteral. siguientes tres literales. Struct :: StructType :: Integer Struct :: StructType :: String Struct :: StructType :: Any Observese que los literales integer, string, any comienzan por minúscula en el metamodelo. RubyTL convierte la primera letra a mayúsculas para hacerlos compatibles con Ruby. Colecciones. Ruby proporciona métodos con un “estilo funcional” (parecidos a los de OCL) para iterar sobre colecciones. Estos métodos normalmente reciben un iterador en forma de bloque de código, que hace algo con cada valor de la colección. Los siguientes métodos pueden ser útiles para este ejemplo (en Ruby una colección se define con []). collection = [1 , 2 , collection . select { collection . reject { collection . find { 3, 4, |i| i |i| i |i| i 5] % 2 == 0 } % 2 == 0 } == 3 } => => => [2 , 4] [1 , 3 , 5] 3 14 E. Solución del ejercicio: árboles de sintaxis concreta transformation ’cst2struct’ decorator CST::Node do def nodes self.children.select { |n| n.kind_of? CST::Node } end def leaves self.children.select { |n| n.kind_of? CST::Leaf } end end rule ’tree2data’ do from CST::Tree to SimpleStruct::Data mapping do |tree, data| data.structs = tree.nodes end end rule ’struct2struct’ do from CST::Node to SimpleStruct::Struct filter { |n| n.kind == ’struct’ } mapping do |node, struct| struct.name = node.leaves.find { |n| n.kind == ’name’ }.value struct.attributes = node.nodes.find { |n| n.kind == ’body’ }.children end end rule ’struct2struct’ do from CST::Node to SimpleStruct::Attribute filter { |n| n.kind == ’attribute’ } mapping do |node, struct| struct.name = node.leaves.find { |n| n.kind == ’name’ }.value type = node.leaves.find { |n| n.kind == ’type’ }.value struct.type = case type.upcase when ’STRING’ then SimpleStruct::StructType::String when ’INTEGER’ then SimpleStruct::StructType::Integer when ’ANY’ then SimpleStruct::StructType::Any end end end 15