Skip to content

Instantly share code, notes, and snippets.

@welvet
Last active December 20, 2015 10:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save welvet/6113603 to your computer and use it in GitHub Desktop.
Save welvet/6113603 to your computer and use it in GitHub Desktop.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DSLRoot {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DSLTypeHint {
Class value();
}
class DSLInvocationError extends Exception {
String reason
String probablyLocation
Exception suppressedException
DSLInvocationError(String reason, String probablyLocation, Exception suppressedException) {
super("${probablyLocation}: ${reason}")
this.reason = reason
this.probablyLocation = probablyLocation
this.suppressedException = suppressedException
}
}
class DSLParser {
String location = DSLParser.package.name
@Lazy
volatile Map<String, Closure> constructors = constructors()
@Lazy
volatile GroovyShell shell = new GroovyShell()
def parse(String data) {
try {
def userScript = shell.parse(data, "UserScript")
userScript.binding = new Binding(constructors)
userScript.run()
} catch (Exception e) {
def find = e.getStackTrace().find { it.fileName == "UserScript" }
throw new DSLInvocationError(e.getMessage(), "Probably error location at line ${find?.lineNumber}", e)
}
}
@Newify
private Map<String, Closure> constructors() {
def result = [:]
new Reflections(location, TypeAnnotationsScanner.new()).getTypesAnnotatedWith(DSLRoot).each {
result[it.getAnnotation(DSLRoot).value()] = { Closure c ->
def root = it.newInstance()
invokeClosureForProperty(root, c)
return root
}
}
return result
}
def invokeClosureForProperty(Object toWrap, Closure closure) {
closure.setDelegate(new DSLWrapper(obj: toWrap))
closure.setResolveStrategy(DELEGATE_ONLY)
closure.call()
return toWrap
}
class DSLWrapper {
Object obj
@Override
Object invokeMethod(String name, Object args) {
try {
return obj.invokeMethod(name, args)
} catch (MissingMethodException ignored) {
//noinspection GroovyAssignabilityCheck
def arg = args[0]
//just process map directly
if (obj instanceof Map) {
obj.put(name, arg)
} else if (obj.hasProperty(name)) {
MetaBeanProperty property = obj.metaClass.properties.find { it.name == name } as MetaBeanProperty
def hint = property.field.field.getAnnotation(DSLTypeHint)?.value()
applyProperty(property, arg, hint)
} else {
throw new IllegalArgumentException("There is no method or property like '${name}' in '${obj.class.name}'")
}
}
return null
}
private void applyProperty(MetaBeanProperty property, arg, Class hint) {
if (property.getType().isAssignableFrom(Map.class)) {
Map map = (property.getProperty(obj) ?: new HashMap<>()) as Map
convertToProperty(HashMap, arg).each { Map.Entry entry ->
map.put(entry.key, convertToProperty(hint, entry.value))
}
property.setProperty(obj, map)
} else if (property.getType().isAssignableFrom(List.class)) {
def list = property.getProperty(obj) ?: new ArrayList<>()
list << convertToProperty(hint, arg)
property.setProperty(obj, list)
} else if (property.getType().isAssignableFrom(Closure)) {
property.setProperty(obj, arg)
} else {
property.setProperty(obj, convertToProperty(property.getType(), arg))
}
}
def convertToProperty(Class type, Object value) {
if (value instanceof Closure) {
if (!type) {throw new IllegalArgumentException("I can't see such a DSLHint around")}
return invokeClosureForProperty(type.newInstance(), value)
} else {
return value.asType(type)
}
}
}
}
class DSLParserTest extends BasicSpec {
def parser
def setup() {
parser = new DSLParser()
parser.location = "dsl.test"
}
def "process simple objects"() {
when:
def house = parser.parse """
pet_house {
name "${houseName}"
master {
name "${houseMaster}"
}
}
"""
then:
house.name == houseName
house.master.name == houseMaster
where:
houseName = "Test name"
houseMaster = "Mr. Meow"
}
def "process list of elements and closures"() {
when:
def house = parser.parse """
pet_house {
pet {
name "${petName1}"
hello {return "Hello from ${petName1}"}
}
pet {
name "${petName2}"
hello {return "Hello from ${petName2}"}
}
}
"""
then:
house.pet.size() == 2
house.pet[0].name == petName1
house.pet[0].hello() == "Hello from ${petName1}"
house.pet[1].name == petName2
house.pet[1].hello() == "Hello from ${petName2}"
where:
petName1 = "Nyan"
petName2 = "Nyan the second"
}
def "process maps"() {
when:
def house = parser.parse """
pet_house {
food {
${food1.name} {
size "${food1.size}"
}
${food2.name} {
size "${food2.size}"
}
}
}
"""
then:
house.food[food1.name].size == food1.size
house.food[food2.name].size == food2.size
where:
food1 = [name: "cat_food", size: 32]
food2 = [name: "dog_food", size: 34]
}
def "guess line with error for you"() {
when:
def house = parser.parse """
pet_house {
prop_non_exists "yep"
}
"""
then:
def error = thrown(DSLInvocationError)
error.probablyLocation.startsWith("Probably error location at line 3")
}
def "and something nice"() {
when:
def house = parser.parse """
pet_house {
10.times {
def id = it
pet {
hello {id}
}
}
}
"""
then:
house.pet.sum {it.hello()} == 45
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment