** Static methods to get and set bean values from property expressions.
class BeanProperties {
// Bad idea, static values are const! (Okay, well, thread safe - but they rarely change anyhow.)
// static Obj getStaticProperty(Type type, Str property) { ... }
** Similar to 'get()' but may read better in code if you know the expression ends with a method.
**
** Any arguments given overwrite arguments in the expression. Example:
**
** BeanProperties.call(Buf(), "fill(255, 4)", [128, 2]) // --> 0x8080
static Obj? call(Obj instance, Str property, Obj?[]? args := null) {
BeanPropertyFactory().parse(property).call(instance, args)
}
** Gets the value of the field (or method) at the end of the property expression.
static Obj? get(Obj instance, Str property) {
BeanPropertyFactory().parse(property).get(instance)
}
** Sets the value of the field at the end of the property expression.
static Void set(Obj instance, Str property, Obj? value) {
BeanPropertyFactory().parse(property).set(instance, value)
}
** Given a map of values, keyed by property expressions, this sets them on the given instance.
**
** Returns the given instance.
static Obj setAll(Obj instance, Str:Obj? propertyValues) {
factory := BeanPropertyFactory()
propertyValues.each |value, expression| {
factory.parse(expression).set(instance, value)
}
return instance
}
** Uses the given property expressions to instantiate a tree of beans and values.
** Nested beans may be 'const' as long as they supply an it-block ctor argument.
static Obj create(Type type, Str:Obj? propertyValues, TypeCoercer? typeCoercer := null, |Type->BeanFactory|? factoryFunc := null) {
factory := BeanPropertyFactory()
if (typeCoercer != null)
factory.typeCoercer = typeCoercer
tree := SegmentTree(null, factoryFunc)
propertyValues.each |value, expression| {
property := factory.parse(expression)
end := (SegmentTree) property.segments[0..<-1].reduce(tree) |SegmentTree mkr, segment -> SegmentTree| {
mkr.branches.getOrAdd(segment.expression) { SegmentTree(segment, factoryFunc) }
}
end.leaves[property.segments[-1]] = value
}
try {
return tree.create(type, null)
} catch (Err err) {
props := propertyValues.map |v, k| { "$k = $v" }.vals
throw BeanCreateErr("Could not instantiate $type.signature".replace("sys::", ""), props, err)
}
}
}
@NoDoc
const class BeanCreateErr : Err, NotFoundErr {
override const Str?[] availableValues
override const Str valueMsg := "Field Values Given:"
new make(Str msg, Obj?[] availableValues, Err? cause := null) : super(msg, cause) {
this.availableValues = availableValues.map { it?.toStr }.sort
}
override Str toStr() {
NotFoundErr.super.toStr
}
}
** Parses property expressions to create 'BeanProperty' instances.
@NoDoc
class BeanPropertyFactory {
// Fantex test string: obj.list[2].map[wot][thing].meth(judge, dredd).str().prop
private static const Regex slotRegex := Regex<|(?:([^\.\[\]\(\)]*)(?:\(([^\)]+)\))?)?(?:\[([^\]]+)\])?|>
** Given to 'BeanProperties' to convert Str values into objects.
** Supplied so you may substitute it with a cached version and / or a more intelligent one that converts IDs to Entities.
TypeCoercer typeCoercer := TypeCoercer()
** Given to 'BeanProperties' to create new instances of intermediate objects.
** Supplied so you may substitute it with one that injects IoC values.
**
** Only used if 'createIfNull' is 'true'.
**
** Defaults to '|Type type->Obj?| { BeanFactory.defaultValue(type, true) }'
|Type->Obj?| makeFunc := |Type type->Obj?| { BeanFactory.defaultValue(type, true) }
** Given to 'BeanProperties' to indicate if they should create new object instances when traversing an expression.
** If an a new instance is *not* created then a 'NullErr' will occur.
**
** For example, in the expression 'a.b.c', should 'b' be null then a new instance is created and set.
** This also applies when setting instances in a 'List' where the List size is less than the index.
**
** Defaults to 'true'
Bool createIfNull := true
** Given to 'BeanProperties' to limit how may list items it may create for any given list.
** If it attempts to grow a list greater than this size then an Err is raised.
**
** Defaults to '10,000'.
**
** Automatically creating 1000 items is mad, but 10,000 is *insane*!
Int maxListSize := 10000
** Parses a property expression stemming from the given type to produce a 'BeanProperty' that can be used to get / set / call the value at the end.
BeanProperty parse(Str property) {
beanSlots := SegmentFactory[,]
matcher := slotRegex.matcher(property)
while (matcher.find) {
if (matcher.group(0).isEmpty) {
continue
}
slotName := matcher.group(1)
methodArgs := matcher.group(2)?.split(',', true)
indexName := matcher.group(3)
beanSlot := (SegmentFactory?) null
if (!slotName.isEmpty)
beanSlots.add(SlotSegment(slotName, methodArgs) {
it.typeCoercer = this.typeCoercer
it.createIfNull = this.createIfNull
it.makeFunc = this.makeFunc
})
if (indexName != null)
beanSlots.add(IndexSegment(indexName) {
it.typeCoercer = this.typeCoercer
it.createIfNull = this.createIfNull
it.makeFunc = this.makeFunc
it.maxListSize = this.maxListSize
})
}
if (beanSlots.isEmpty)
throw Err("Could not parse property string: ${property}")
return BeanProperty(property, beanSlots)
}
}
** Calls methods and gets and sets fields at the end of a property expression.
** Property expressions may traverse lists, maps and methods.
** All field are accessed through their respective getter and setters.
**
** Use `BeanPropertyFactory` to create instances of 'BeanProperty'.
@NoDoc
const class BeanProperty {
** The property expression that this class ultimately calls.
const Str expression
internal const SegmentFactory[] segments
internal new make(Str expression, SegmentFactory[] segments) {
this.expression = expression
this.segments = segments
}
** Similar to 'get()' but may read better in code if you know the expression ends with a method.
**
** Any arguments given overwrite arguments in the expression. Example:
**
** BeanProperties.call(Buf(), "fill(255, 4)", [128, 2]) // --> 0x8080
Obj? call(Obj instance, Obj?[]? args := null) {
callChain(instance).get(args)
}
** Gets the value of the field (or method) at the end of the property expression.
@Operator
Obj? get(Obj instance) {
callChain(instance).get(null)
}
** Sets the value of the field at the end of the property expression.
@Operator
Void set(Obj instance, Obj? value) {
callChain(instance).set(value)
}
internal Str expressionParent() {
segments[0..<-1].join(".")
}
private SegmentExecutor callChain(Obj instance) {
staticType := instance.typeof
segments.eachRange(0..<-1) |bean| {
segment := bean.makeSegment(staticType, instance, false)
instance = segment.get(null)
staticType = segment.returns
}
return segments[-1].makeSegment(staticType, instance, true)
}
@NoDoc
override Str toStr() {
segments.join(".")
}
}