Last active
December 20, 2015 10:09
-
-
Save welvet/6113603 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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