** Models a Fantom class.
**
** If not defined already, types are generated with a standard it-block ctor:
**
** syntax: fantom
** new make(|This|? f := null) { f?.call(this) }
**
** All added fields and methods will be public. As you will never compile against the generated
** code, this should not be problematic.
// Class Model is correct, to be paired with MixinModel!
class PlasticClassModel {
** Set to 'true' if this class is 'const'
const Bool isConst
** The name of the class.
const Str className
** The superclass type.
Type superClass := Obj# { private set }
** A list of mixin types the model extends.
Type[] mixins := [,] { private set } // for user info only
PlasticUsingModel[] usings { private set }
PlasticFacetModel[] facets { private set }
PlasticFieldModel[] fields { private set }
PlasticMethodModel[] methods { private set }
PlasticCtorModel[] ctors { private set }
private Type[] extends() {
[superClass].addAll(mixins)
}
** Creates a class model with the given name.
new make(Str className, Bool isConst) {
this.isConst = isConst
this.className = className
this.usings = [,]
this.facets = [,]
this.fields = [,]
this.methods = [,]
this.ctors = [,]
}
** 'use' the given pod.
This usingPod(Pod pod) {
usings.add(PlasticUsingModel(pod))
return this
}
** 'use' the given type.
This usingType(Type type, Str? usingAs := null) {
usings.add(PlasticUsingModel(type, usingAs))
return this
}
** 'use' the given Str, should not start with using.
This usingStr(Str usingStr) {
usings.add(PlasticUsingModel(usingStr))
return this
}
PlasticFacetModel addFacet(Type type, Str:Str params := [:]) {
facetModel := PlasticFacetModel(type, params)
facets.add(facetModel)
return facetModel
}
This addFacetClone(Facet toClone) {
facets.add(PlasticFacetModel(toClone))
return this
}
** Extends the given type; be it a class or a mixin.
**
** If 'type' is a class, it is set as the superclass, if it is a mixin, it is extended.
**
** If this model is const, then the given type must be const also.
**
** The type must be public.
This extend(Type type) {
// this is actually fine - const classes CAN extend non-const mixins
// if (isConst && !type.isConst)
// throw PlasticErr(PlasticMsgs.constTypeCannotSubclassNonConstType(className, type))
if (!isConst && type.isConst)
throw PlasticErr("Non-const type ${className} can not subclass const type ${type.qname}")
if (type.isInternal)
throw PlasticErr("Super types must be 'public' or 'protected' scope - class ${className} : ${type.qname}")
if (type.isClass) {
if (superClass != Obj# && superClass != type)
throw PlasticErr("Can not set Supertype to ${type.qname}, it is already set to ${superClass.qname}")
superClass = type
}
if (type.isMixin) {
// need to be clever about what mixin we add
// see http://fantom.org/sidewalk/topic/2216
if (mixins.any { it.fits(type) })
return this
mixins := this.mixins.exclude { type.fits(it) }
mixins.add(type)
this.mixins = mixins.unique
}
return this
}
@NoDoc @Deprecated { msg="Use extend() instead" }
This extendClass(Type classType) {
extend(classType)
}
@NoDoc @Deprecated { msg="Use extend() instead" }
This extendMixin(Type mixinType) {
extend(mixinType)
}
** Add a field.
** 'getBody' and 'setBody' are code blocks to be used in the 'get' and 'set' accessors.
PlasticFieldModel addField(Type fieldType, Str fieldName, Str? getBody := null, Str? setBody := null) {
// synthetic fields may be non-const - how do we check if field is synthetic?
// if (isConst && !fieldType.isConst)
// throw PlasticErr(PlasticMsgs.constTypesMustHaveConstFields(className, fieldType, fieldName))
fieldModel := PlasticFieldModel(false, PlasticVisibility.visPublic, isConst, fieldType, fieldName, getBody, setBody)
fields.add(fieldModel)
return fieldModel
}
** Override a field.
** The given field must exist in a super class / mixin.
** 'getBody' and 'setBody' are code blocks to be used in the 'get' and 'set' accessors.
PlasticFieldModel overrideField(Field field, Str? getBody := null, Str? setBody := null) {
if (!extends.any { it.fits(field.parent) })
throw PlasticErr("Field ${field.qname} does not belong to super type " + extends.map { it.qname }.join(", "))
if (field.isPrivate || field.isInternal)
throw PlasticErr("Field ${field.qname} must have 'public' or 'protected' scope")
fieldModel := PlasticFieldModel(true, PlasticVisibility.visPublic, field.isConst, field.type, field.name, getBody, setBody)
fields.add(fieldModel)
return fieldModel
}
** Returns 'true' if this model has a field with the given name.
Bool hasField(Str name) {
fields.any { it.name == name }
}
** Add a method.
** 'signature' does not include (brackets).
** 'body' does not include {braces}
PlasticMethodModel addMethod(Type returnType, Str methodName, Str signature, Str body) {
methodModel := PlasticMethodModel(false, PlasticVisibility.visPublic, returnType, methodName, signature, body)
methods.add(methodModel)
return methodModel
}
** Override a method.
** The given method must exist in a super class / mixin.
** 'body' does not include {braces}
PlasticMethodModel overrideMethod(Method method, Str body) {
if (!extends.any { it.fits(method.parent) })
throw PlasticErr(overrideMethodDoesNotBelongToSuperType(method, extends))
if (method.isPrivate || method.isInternal)
throw PlasticErr("Method ${method.qname} must have 'public' or 'protected' scope")
if (!method.isVirtual)
throw PlasticErr("Method ${method.qname} must be virtual (or abstract)")
params := method.params.map |param->Str| {
pSig := "${param.type.signature} ${param.name}"
if (param.hasDefault) {
def := guessDefault(method, param)
if (def == null)
throw PlasticErr("Can not determine a default parameter value for param '${param.name}' of : ${method.qname}")
pSig += " := ${def}"
}
return pSig
}
methodModel := PlasticMethodModel(true, PlasticVisibility.visPublic, method.returns, method.name, params.join(", "), body)
methods.add(methodModel)
return methodModel
}
** Add a ctor.
**
** 'signature' does not include (brackets).
**
** 'body' does not include {braces}
**
** 'superCtor' is the entire expression, 'super.make(in)'
PlasticCtorModel addCtor(Str ctorName, Str signature, Str body, Str? superCtor := null) {
ctorModel := PlasticCtorModel(PlasticVisibility.visPublic, ctorName, signature, body, superCtor)
ctors.add(ctorModel)
return ctorModel
}
** Override a ctor
** The given ctor method must exist in a super class / mixin.
** 'body' does not include {braces}
PlasticCtorModel overrideCtor(Method ctor, Str body) {
if (!extends.any { it.fits(ctor.parent) })
throw PlasticErr(overrideMethodDoesNotBelongToSuperType(ctor, extends))
if (ctor.isPrivate || ctor.isInternal)
throw PlasticErr("Method ${ctor.qname} must have 'public' or 'protected' scope")
params := ctor.params.map |param->Str| {
pSig := "${param.type.signature} ${param.name}"
if (param.hasDefault) {
def := guessDefault(ctor, param)
if (def == null)
throw PlasticErr("Can not determine a default parameter value for param '${param.name}' of : ${ctor.qname}")
pSig += " := ${def}"
}
return pSig
}
paramSig := params.join(", ")
paramNom := ctor.params.map { it.name }.join(", ")
ctorModel := PlasticCtorModel(PlasticVisibility.visPublic, ctor.name, paramSig, body, "super.${ctor.name}(${paramNom})")
ctors.add(ctorModel)
return ctorModel
}
** Converts the model into Fantom source code.
**
** All types are generated with a standard serialisation ctor:
**
** syntax: fantom
** new make(|This|? f := null) { f?.call(this) }
Str toFantomCode() {
typeCache := TypeCache()
// add a useful default ctor if it doesn't exist
if (superClass == Obj# && ctors.any { it.name == "make" }.not)
addCtor("make", "|This|? f := null", "f?.call(this)")
code := StrBuf()
facets.each { code.add(it.toFantomCode) }
constKeyword := isConst ? "const " : ""
extendsKeyword := extends.exclude { it == Obj#}.isEmpty ? "" : " : " + extends.unique.exclude { it == Obj#}.map { it.qname }.join(", ")
code.add("${constKeyword}class ${className}${extendsKeyword} {\n\n")
fields .each { code.add(it.toFantomCode(typeCache)) }
ctors .each { code.add(it.toFantomCode) }
methods .each { code.add(it.toFantomCode(typeCache)) }
code.add("}\n")
typeCache.addTo(usings)
useStr := StrBuf()
usings.unique.each { useStr.add(it.toFantomCode) }
useStr.addChar('\n')
code.insert(0, useStr.toStr)
return code.toStr
}
internal Str? guessDefault(Method method, Param param) {
if (method.isStatic || method.isCtor)
try {
// new in Fantom 1.0.68 !!!
def := method.paramDef(param)
if (def != null) {
toCode := ReflectUtils.findMethod(param.type, "toCode", null, false, Str#)
if (toCode != null)
return toCode.callOn(def, null)
if (def.typeof.hasFacet(Serializable#)) {
sBuf := StrBuf()
sBuf.clear.out.writeObj(def).close
return sBuf.toStr
}
}
} catch (Err e) { /* meh */ }
// Special case for Bool checked
if (param.type == Bool# && param.name.equalsIgnoreCase("checked"))
return true.toStr
try {
// nullable values
defVal := BeanBuilder.defVal(param.type)
if (defVal == null)
return "null"
// types with a defVal and a toCode() method
toCode := ReflectUtils.findMethod(param.type, "toCode", null, false, Str#)
if (toCode != null)
return toCode.callOn(defVal, null)
} catch
return null
// types with a default ctor
ctor := ReflectUtils.findCtors(param.type)
if (ctor.size == 1)
return "${param.type.qname}()"
return null
}
private static Str overrideMethodDoesNotBelongToSuperType(Method method, Type[] superTypes) {
"Method ${method.qname} does not belong to super types " + superTypes.map { it.qname }.join(", ")
}
}