diff --git a/typescript_templates/model.vm b/typescript_templates/model.vm index 4c761ce..346088d 100644 --- a/typescript_templates/model.vm +++ b/typescript_templates/model.vm @@ -9,9 +9,13 @@ $str.kebabToCamel($param.propertyName.replaceAll("_", "-"))## #set( $type_override_variable = "${d}${e}propFile.type_override_${className}_#paramName(${param})" ) #set( $type_override = "#evaluate($type_override_variable)" ) #if ( $param.algorandFormat == "SignedTransaction" ) -EncodedSignedTransaction## -#elseif ( $param.algorandFormat == "Address" ) ## No special handling for Address in go SDK -string## +SignedTransaction## +#elseif ( $param.algorandFormat == "Address" ) +#if ( $isArgType ) +(Address | string)## +#else +Address## +#end #elseif ( $param.algorandFormat == "BlockHeader" ) BlockHeader## #elseif ( $type_override == "bigint" || ( $param.algorandFormat == "uint64" && $type_override.length() == 0 ) ) @@ -21,7 +25,7 @@ BlockHeader## bigint## #end #elseif ( $param.type == "object" ) -Record## +UntypedValue## #elseif ( $type_override == "number" || ( ( $param.type == "integer" || $param.arrayType == "integer" ) && $type_override.length() == 0 ) ) #if ( $isArgType ) (number | bigint)## @@ -46,7 +50,7 @@ Uint8Array## string## #elseif( $param.arrayType ) ${param.arrayType}## -#elseif( $param.refType ) ## This is second because the old code avoids typedef of references +#elseif( $param.refType )## This is second because the old code avoids typedef of references ${param.refType}## #else UNHANDLED TYPE @@ -59,6 +63,50 @@ $unknown.type ## force a template failure with an unknown type #end #if ($param.arrayType && $param.arrayType != "")[]#end## Add array postfix to arrays... #end +## Gets the Schema object for a given type. +#macro ( toSchema $param ) +#if ( !$param.required ) +new OptionalSchema(## +#end +#if ( $param.arrayType ) +new ArraySchema(## +#end +#if ( $param.algorandFormat == "BlockHeader" ) +BLOCK_HEADER_SCHEMA## +#elseif ( $param.algorandFormat == "SignedTransaction" ) +SignedTransaction.encodingSchema## +#elseif ( $param.type == "object" || $param.arrayType == "object" ) +UntypedValue.encodingSchema## +#elseif ( $param.algorandFormat == "Address" ) +new StringSchema()## # To comply with existing msgpack REST API behavior, encode addresses as strings +#elseif ( $param.algorandFormat == "uint64" || $param.type == "integer" || $param.arrayType == "integer" ) +new Uint64Schema()## +#elseif ( $param.type == "boolean" || $param.arrayType == "boolean" ) +new BooleanSchema()## +#elseif( ( $param.type == "string" || $param.arrayType == "string" ) && ( $param.format == "byte" || $param.format == "binary" ) ) +new ByteArraySchema()## +#elseif( $param.type == "string" || $param.arrayType == "string" ) +new StringSchema()## +#elseif ( "#isClassType($param)" == "true" ) +#if ( $param.arrayType ) +${param.arrayType}## +#else +${param.refType}## +#end +.encodingSchema## +#else +UNHANDLED SCHEMA TYPE +- property: $param +- isClassType: #isClassType($param) +$unknown.type ## force a template failure with an unknown type +#end +#if ( $param.arrayType ) +)## +#end +#if ( !$param.required ) +)## +#end +#end ## Check if there's a class associated with this type #macro ( isClassType $param ) #if ( $param.algorandFormat == "SignedTransaction" ) @@ -89,6 +137,23 @@ false## true## #end #end +## Returns "true" if the type is a primative, meaning it's not an object that requires manipulation +## when converting to & from encoding data. +#macro ( isPrimativeType $param ) +#if ( $param.algorandFormat == "uint64" || $param.type == "integer" || $param.arrayType == "integer" ) +true## +#elseif ( $param.type == "boolean" || $param.arrayType == "boolean" ) +true## +#elseif( $param.type == "string" || $param.arrayType == "string" ) +#if ( $param.algorandFormat == "Address" || $param.algorandFormat == "SignedTransaction" ) +false## +#else +true## +#end +#else +false## +#end +#end ## Create an expression to assign a field in a constructor #macro ( constructorAssignType $className $prop ) #set( $argType = "#toSdkType($className, $prop, true)" ) @@ -96,6 +161,14 @@ true## #set( $name = "#paramName($prop)" ) #if ( $argType == $fieldType ) $name## +#elseif ( $argType == "(Address | string)" && $fieldType == "Address" ) +typeof $name === 'string' ? Address.fromString($name) : $name## +#elseif ( $argType == "(Address | string)[]" && $fieldType == "Address[]" ) +#if ( $prop.required ) +${name}.map(addr => typeof addr === 'string' ? Address.fromString(addr) : addr)## +#else +typeof $name !== 'undefined' ? ${name}.map(addr => typeof addr === 'string' ? Address.fromString(addr) : addr) : undefined## +#end #elseif ( $argType == "string | Uint8Array" && $fieldType == "Uint8Array" ) typeof $name === 'string' ? base64ToBytes($name) : $name## #elseif ( $argType == "(number | bigint)" && $fieldType == "bigint" ) @@ -131,24 +204,73 @@ UNHANDLED CONSTRUCTOR TYPE CONVERSION $unknown.type ## force a template failure with an unknown type #end #end -## Create an expression to assign a field in the from_obj_for_encoding function -#macro ( fromObjForEncodingAssignType $value $prop ) -#if ( "#isClassType($prop)" == "false" ) +## Create an expression to assign a field in the toEncodingData function +#macro ( fieldToEncodingData $prop ) +#set( $value = "this.#paramName($prop)" ) +#set( $isPrimative = "#isPrimativeType($prop)" == "true" ) +#set( $needsUndefinedCheck = !$isPrimative && !$prop.required ) +#if ( $needsUndefinedCheck ) +typeof ${value} !== "undefined" ? ## +#end +#if ( $prop.arrayType && !$isPrimative )## No need to map a primative type +${value}.map(v => ## +#set ( $value = "v" ) +#end +#if ( $prop.algorandFormat == "BlockHeader" ) +blockHeaderToEncodingData($value)## +#elseif ( $prop.algorandFormat == "Address" ) +${value}.toString()## +#elseif ( $isPrimative ) $value## -#elseif ( $prop.arrayType ) -#set ( $assignment = "${value}.map(${prop.arrayType}.from_obj_for_encoding)" ) -#if ($prop.required) -$assignment## #else -typeof $value !== 'undefined' ? $assignment : undefined## +${value}.toEncodingData()## +#end +#if ( $prop.arrayType && !$isPrimative ) +)## +#end +#if ( $needsUndefinedCheck ) +: undefined## +#end +#end +## Create an expression to assign a field in the fromEncodingData function +#macro ( fromEncodingDataAssignType $value $prop ) +#set( $isPrimative = "#isPrimativeType($prop)" == "true" || $prop.algorandFormat == "Address" )## Addresses are encoded as strings, so treat them as primatives here +#set( $needsUndefinedCheck = !$isPrimative && !$prop.required ) +#if ( $needsUndefinedCheck ) +typeof ${value} !== "undefined" ? ## +#end +#if ( $prop.arrayType && !$isPrimative ) +#if ( $prop.required ) +(${value} ?? [])## +#else +$value## +#end +.map((v: unknown) => ## +#set ( $value = "v" ) +#elseif ( $prop.required && !$isPrimative ) +#set ( $value = "($value ?? new Map())" )## #end +#if ( $prop.algorandFormat == "BlockHeader" ) +blockHeaderFromEncodingData($value)## +#elseif ( $isPrimative ) +${value}## #else -#set ( $assignment = "${prop.refType}.from_obj_for_encoding($value)" ) -#if ($prop.required) -$assignment## +#if ( $prop.algorandFormat == "SignedTransaction" ) +SignedTransaction## +#elseif ( $prop.type == "object" ) +UntypedValue## +#elseif ( $prop.arrayType ) +${prop.arrayType}## #else -typeof $value !== 'undefined' ? $assignment : undefined## +${prop.refType}## +#end +.fromEncodingData($value) #end +#if ( $prop.arrayType && !$isPrimative ) +)## +#end +#if ( $needsUndefinedCheck ) +: undefined## #end #end #macro ( questionMarkIfOptional $param ) @@ -166,12 +288,15 @@ typeof $value !== 'undefined' ? $assignment : undefined## /* eslint-disable no-use-before-define */ import { ensureBigInt, ensureSafeInteger } from '../../../../utils/utils.js'; +import { Encodable, Schema } from '../../../../encoding/encoding.js'; +import { NamedMapSchema, ArraySchema, Uint64Schema, StringSchema, BooleanSchema, ByteArraySchema, OptionalSchema } from '../../../../encoding/schema/index.js'; import { base64ToBytes } from '../../../../encoding/binarydata.js'; #if ( $propFile.indexer == "false" ) -import BlockHeader from '../../../../types/blockHeader.js'; -import { EncodedSignedTransaction } from '../../../../types/transactions/encoded.js'; +import BlockHeader, { blockHeaderFromEncodingData, blockHeaderToEncodingData, BLOCK_HEADER_SCHEMA } from '../../../../types/blockHeader.js'; +import { SignedTransaction } from '../../../../signedTransaction.js'; #end -import BaseModel from '../../basemodel.js'; +import { Address } from '../../../../encoding/address.js'; +import { UntypedValue } from '../../untypedmodel.js'; #foreach( $modelEntry in $models.entrySet() ) #set( $def = $modelEntry.key ) @@ -190,7 +315,24 @@ import BaseModel from '../../basemodel.js'; * $str.formatDoc($def.doc, " * ") */ #end -export class $def.name extends BaseModel { +export class $def.name implements Encodable { + + private static encodingSchemaValue: Schema | undefined; + + static get encodingSchema(): Schema { + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); +## By assigning a value to this.encodingSchemaValue before getting the .encodingSchema fields of other types, +## we allow circular references to be handled properly. + (this.encodingSchemaValue as NamedMapSchema).entries.push( +#foreach( $prop in $props ) + { key: '$prop.propertyName', valueSchema: #toSchema($prop), omitEmpty: true }, +#end + ); + } + return this.encodingSchemaValue; + } + #foreach( $prop in $props ) #if ( !$prop.doc.isEmpty() ) /** @@ -228,48 +370,43 @@ export class $def.name extends BaseModel { #else ) { #end - super(); #foreach( $prop in $props ) #set( $var = "#paramName($prop)" ) this.$var = #constructorAssignType($def.name, $prop); #end + } + + // eslint-disable-next-line class-methods-use-this + getEncodingSchema(): Schema { + return ${def.name}.encodingSchema; + } - this.attribute_map = { + toEncodingData(): Map { + return new Map([ #foreach( $prop in $props ) - #paramName($prop): '$prop.propertyName', + ['$prop.propertyName', #fieldToEncodingData($prop)], #end - } + ]); } - // eslint-disable-next-line camelcase - static from_obj_for_encoding(data: Record): $def.name { - /* eslint-disable dot-notation */ + static fromEncodingData(data: unknown): $def.name { #set ( $d = "$" )## Create a variable in order to insert a $ into the code -#foreach( $prop in $props ) -#if ($prop.required) -#if ($prop.arrayType) - if (!Array.isArray(data['$prop.propertyName'])) - throw new Error(`Response is missing required array field '${prop.propertyName}': ${d}{data}`); -#else - if (typeof data['$prop.propertyName'] === 'undefined') - throw new Error(`Response is missing required field '${prop.propertyName}': ${d}{data}`); -#end -#end -#end + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded logic sig account: ${d}{data}`); + } #if ($use_object_params) return new ${def.name}({ #foreach( $prop in $props ) - #paramName($prop): #fromObjForEncodingAssignType("data['$prop.propertyName']", $prop), + #paramName($prop): #fromEncodingDataAssignType("data.get('$prop.propertyName')", $prop), #end }); #else return new ${def.name}( #foreach( $prop in $props ) - #fromObjForEncodingAssignType("data['$prop.propertyName']", $prop), + #fromEncodingDataAssignType("data.get('$prop.propertyName')", $prop), #end ); #end - /* eslint-enable dot-notation */ } }