All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.blackbuild.groovy.configdsl.transform.DSL Maven / Gradle / Ivy

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2024 Stephan Pauxberger
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.blackbuild.groovy.configdsl.transform;

import com.blackbuild.klum.cast.KlumCastValidated;
import com.blackbuild.klum.cast.KlumCastValidator;
import groovy.transform.Undefined;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 

DSL is used to designate a DSL/Model object, which is enriched using the AST transformation. The annotation can also be placed on interfaces, however, interfaces are not transformed at all, fields with an DSL interface type are still handled correctly.

The DSL annotation leads to the creation of a couple of useful DSL methods. Note that most of these methods are not visible by default, as not to clutter the interface of the model. Instead they are created in a special inner class that is only accessible with

Factory and {@code apply} methods

Each instantiable DSL class gets a static field {@code Create} of either a subclass of KlumFactory.Keyed or KlumFactory.Unkeyed, which provides methods to create instances of the class; abstract classed get an implementation of KlumFactory instead.


 {@literal @}DSL
 class Config {}

 {@literal @}DSL
 class ConfigWithKey {
   {@literal @Key} String name
 }
 

allows to create instances with the following calls:


 Config.Create.One()
 Config.Create.With(a: 1, b: 2)
 Config.Create.With(a: 1, b: 2) { c 3 }
 Config.Create.With { c 3 }

 ConfigWithKey.Create.One('Dieter')
 ConfigWithKey.Create.With('Dieter', a: 1, b: 2)
 ConfigWithKey.Create.With('Dieter', a: 1, b: 2) { c 3 }
 ConfigWithKey.Create.With('Dieter') { c 3 }
 
The optional closure to the {@code With} method is used to set values on the created object. The 'One' method is a shortcut for 'With' without any given values, which makes a nicer syntax ({@code Config.Create.With()} seems a bit strange).

Note that pre 2.0 versions of KlumAST did create the methods directly as static methods of the model class. These methods are now deprecated in will be removed in a future version.

If the class contains an static inner class named Factory of the appropriate type or the member factoryBase points to such a class, this class is used as a base for the generated factory instead. This allows adding additional methods to the factory.

Additionally, an {@code apply} method is created, which takes single closure and applies it to an existing object.


 def void apply(Closure c)
 

Both {@code apply} and {@code create} also support named parameters, allowing to set values in a concise way. Every map element of the method call is converted in a setter call (actually, any method named like the key with a single argument will be called):


 Config.Create.With {
   name "Dieter"
   age 15
 }
 

Could also be written as:


 Config.Create.With(name: 'Dieter', age: 15)
 

Of course, named parameters and regular calls inside the closure can be combined ad lib.

There are also a couple of [[Convenience Factories]] to load a model into client code.

Lifecycle Methods

Lifecycle methods can are methods annotated with {@code {@literal @PostCreate}} and {@code {@literal @PostApply}}. These methods will be called automatically after the creation of the object (**after the [[template|Templates]] - if set - has been applied**) and after the call to the apply method, respectively.

Lifecycle methods must not be {@code private} and will automatically be made protected, which means you can usually safely use default groovy visibility (i.e. simply use {@code def myMethod()}).

copyFrom() method

Each DSLObject gets a {@code copyFrom()} method with its own class as parameter. This method copies fields from the given object over to this objects, excluding key and owner fields. For non collection fields, only a reference is copied, for Lists and Maps, shallow copies are created.

Currently, it is in discussion whether this should be deep clone instead, see: (#36)

equals() and toString() methods

If not yet present, {@code equals()} and {@code toString()} methods are generated using the respective ASTTransformations. You can customize them by using the original ASTTransformations.

hashCode()

A barebone hashcode is created, with a constant 0 for non-keyed objects, and the hashcode of the key for keyed objects. While this is correct and works with changing objects after adding them to a HashSet / HashMap, the performance for Sets of non-Keyed objects is severely reduced.

Field setter

Field setter for simple fields

For each simple value field create an accessor named like the field, containing the field type as parameter.


 {@literal @}DSL
 class Config {
   String name
 }
 

creates the following method:


 def name(String value)
 

Used by:


 Config.Create.With {
   name "Hallo"
 }
 

Setter for simple collections

for each simple collection, two/three methods are generated:

  • two methods with the collection name and a Iterable/Vararg argument for Collections or a Map argument for maps. These methods *add* the given parameters to the collection
  • an adder method named like the element name of the collection an containing a the element type

 {@literal @}DSL
 class Config {
   {@code List} roles
   {@code Map} levels
 }
 

creates the following methods:


 def roles(String... values)
 def roles({@code Iterable} values)
 def role(String value)
 def levels(Map levels)
 def level(String key, Integer value)
 

Usage:


 Config.Create.With {
   roles "a", "b"
   role "another"
   levels a:5, b:10
   level "high", 8
 }
 

If the collection has no initial value, it is automatically initialized.

Setters and closures for DSL-Object Fields

for each dsl-object field, a closure method is generated, if the field is a keyed object, this method has an additional String parameter. Also, a regular setter method is created for reusing an existing object.


 {@literal @}DSL
 class Config {
   UnKeyed unkeyed
   Keyed keyed
 }

 {@literal @}DSL
 class UnKeyed {
   String name
 }

 {@literal @}DSL
 class Keyed {
 {@literal @}Key String name
 String value
 }
 

creates the following methods (in Config):


 def unkeyed(UnKeyed reuse) // reuse an exiting object
 Unkeyed unkeyed({@literal @}DelegatesTo(Unkeyed) Closure closure)
 def keyed(UnKeyed reuse) // reuse an exiting object
 Keyed keyed(String key, {@literal @}DelegatesTo(Unkeyed) Closure closure)
 

Usage:


 Config.Create.With {
   unkeyed {
     name "other"
   }
   keyed("klaus") {
     value "a Value"
   }
 }

 def objectForReuse = UnKeyed.Create.With { name = "reuse" }

 Config.Create.With {
   unkeyed objectForReuse
 }
 

The closure methods return the created objects, so you can also do the following:


 def objectForReuse
 Config.Create.With {
   objectForReuse = unkeyed {
     name "other"
   }
 }

 Config.Create.With {
   unkeyed objectForReuse
 }
 

Collections of DSL Objects

Collections of DSL-Objects are created using a nested closure. The name of the (optional) outer closure is the field name, the name of the inner closures the element name (which defaults to field name minus a trailing 's'). The syntax for adding keyed members to a list and to a map is identical (obviously, only keyed objects can be added to a map).

The inner creator can also take an existing object instead of a closure, which adds that object to the collection. In that case, the owner field of the added object is only set, when it does not yet have an owner.

This syntax is especially useful for delegating the creation of objects into a separate method.

As with simple objects, the inner closures return the existing object for reuse


 {@literal @}DSL
 class Config {
   {@code List} elements
   {@code List} keyedElements
   {@code Map} mapElements
 }

 {@literal @}DSL
 class UnKeyed {
   String name
 }

 {@literal @}DSL
 class Keyed {
   {@literal @}Owner owner
   {@literal @}Key String name
   String value
 }

 def objectForReuse = UnKeyed.Create.With { name "reuse" }
 def anotherObjectForReuse

 def createAnObject(String name, String value) {
   Keyed.Create.With(name) { value(value) }
 }

 Config.Create.With {
   elements {
     element {
        name "an element"
     }
     element {
       name "another element"
     }
       element objectForReuse
     }
     keyedElements {
       anotherObjectForReuse = keyedElement ("klaus") {
       value "a Value"
     }
   }
   mapElements {
     mapElement ("dieter") {
       value "another"
     }
     mapElement anotherObjectForReuse // owner is NOT changed
     mapElement createAnObject("Hans", "Franz") // owner is set to Config instance
   }
 }

 // flat syntax without nested closures:
 Config.Create.With {
   element {
     name "an element"
   }
   element {
     name "another element"
   }
   element objectForReuse
   anotherObjectForReuse = keyedElement ("klaus") {
     value "a Value"
   }
   mapElement ("dieter") {
     value "another"
   }
   mapElement anotherObjectForReuse // owner is NOT changed
   mapElement createAnObject("Hans", "Franz") // owner is set to Config instance
 }
 

On collections

Although most examples in this wiki use {@code List}, basically any class implementing / sub interface of {@code Collection} can be used instead. There are a couple of points to take note, however:

  • The default Java Collection Framework interfaces (Collection, List, Set, SortedSet, Stack, Queue) work out of the box
  • When using a custom collection **class** or **interface**, in order for initial values to be provided, {@code List} must be coerced to your custom type, i.e. the code {@code [] as } must be resolvable. This can be done by
    • enhance the {@code List.asType()} method to handle your custom type
    • in case of a custom class, provide a constructor taking an {@code Iterable} (or {@code Collection} or {@code List}) argument

However, it is strongly advised to only take the basic interfaces. If additional functionality is needed, it might make more sense to apply it using a decorator (for example using KlumWrap) after the object is constructed.

For maps, only {@code Map} and {@code SortedMap} is supported.

Be careful when using a simple {@code Set}. Since Klum creates barebone hashcode implementations (constant zero for non-keyed objects, hashCode of key for keyed objects), a (non Sorted){@code Set} of non-Keyed model objects might result in a severe degradation of performance of that Set.

*/ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited // This is currently not used, see https://issues.apache.org/jira/browse/GROOVY-6765 @GroovyASTTransformationClass({ "com.blackbuild.groovy.configdsl.transform.ast.DSLASTTransformation", "com.blackbuild.groovy.configdsl.transform.ast.mutators.ModelVerifierTransformation", "com.blackbuild.groovy.configdsl.transform.ast.DelegatesToRWTransformation", }) @KlumCastValidated @KlumCastValidator("com.blackbuild.klum.ast.validation.CheckDslDefaultImpl") @Documented public @interface DSL { /** * The short name of the class to be used in collections. If not set, defaults to the name of * the class, with the first character converted to lowercase. */ String shortName() default ""; /** * When present, the given suffix is stripped from child class names to determine the short name. */ String stripSuffix() default ""; /** * When present, the given type is used as default type for a field of this type. * This makes most sense on interfaces or abstract classes. */ Class defaultImpl() default Undefined.class; /** * When set, the given class, which must be a subclass of either KlumFactory (for abstract classes) or * KlumFactory.Keyed/Unkeyed will be used as a base for the generated factory class. Note that if the annotated class * contains a static inner class named "Factory", this class will be used by default. */ Class factoryBase() default Undefined.class; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy