Skip to content

Commit 5466ac9

Browse files
committed
Support Optional in a constructor of the SCORE
1 parent 214f104 commit 5466ac9

File tree

5 files changed

+216
-80
lines changed

5 files changed

+216
-80
lines changed

unittest/src/main/java/com/iconloop/score/test/GenerateTScoreProcessor.java

+25-21
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ private TypeSpec typeSpec(ClassName className, TypeElement typeElement) {
172172
builder.addMethod(
173173
MethodSpec.constructorBuilder()
174174
.addModifiers(ee.getModifiers())
175-
.addParameters(ProcessorUtil.getParameterSpecs(ee))
175+
.addParameters(GenerateTScoreProcessor.getParameterSpecs(typeElement, ee))
176176
.addStatement("super($L)", paramJoin(paramNames(ee)))
177177
.build());
178178
}
@@ -224,6 +224,29 @@ Context.class, TypeConverter.class, objectArray(indexed),
224224
return null;
225225
}
226226

227+
private static List<ParameterSpec> getParameterSpecs(TypeElement typeElement, ExecutableElement ee) {
228+
boolean mustOptional = false;
229+
List<ParameterSpec> params = new ArrayList<>();
230+
for (VariableElement ve : ee.getParameters()) {
231+
ParameterSpec param;
232+
if (ve.getAnnotation(Optional.class) != null) {
233+
param = ParameterSpec.builder(TypeName.get(ve.asType()), ve.getSimpleName().toString())
234+
.addModifiers(ve.getModifiers())
235+
.addAnnotation(TOptional.class)
236+
.build();
237+
mustOptional = true;
238+
} else {
239+
if (mustOptional) {
240+
throw new RuntimeException(String.format("parameter should be optional, %s of %s.%s",
241+
ve.getSimpleName(), typeElement.getSimpleName(), ee.getSimpleName()));
242+
}
243+
param = ParameterSpec.get(ve);
244+
}
245+
params.add(param);
246+
}
247+
return params;
248+
}
249+
227250
private MethodSpec externalMethodSpec(ExecutableElement ee, TypeElement typeElement) {
228251
External ann = ee.getAnnotation(External.class);
229252
if (ann != null) {
@@ -233,26 +256,7 @@ private MethodSpec externalMethodSpec(ExecutableElement ee, TypeElement typeElem
233256
throw new RuntimeException(String.format("readonly method cannot be payable, %s.%s",
234257
typeElement.getSimpleName(), ee.getSimpleName()));
235258
}
236-
boolean mustOptional = false;
237-
List<ParameterSpec> params = new ArrayList<>();
238-
for (VariableElement ve : ee.getParameters()) {
239-
ParameterSpec param;
240-
if (ve.getAnnotation(Optional.class) != null) {
241-
param = ParameterSpec.builder(TypeName.get(ve.asType()), ve.getSimpleName().toString())
242-
.addModifiers(ve.getModifiers())
243-
.addAnnotation(TOptional.class)
244-
.build();
245-
mustOptional = true;
246-
} else {
247-
if (mustOptional) {
248-
throw new RuntimeException(String.format("parameter should be optional, %s of %s.%s",
249-
ve.getSimpleName(), typeElement.getSimpleName(), ee.getSimpleName()));
250-
}
251-
param = ParameterSpec.get(ve);
252-
}
253-
params.add(param);
254-
}
255-
259+
var params = GenerateTScoreProcessor.getParameterSpecs(typeElement, ee);
256260
TypeName returnType = TypeName.get(ee.getReturnType());
257261
MethodSpec.Builder builder = MethodSpec.methodBuilder(ee.getSimpleName().toString())
258262
.addModifiers(ee.getModifiers())

unittest/src/main/java/score/ServiceManagerImpl.java

+108-52
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import score.impl.TypeConverter;
3232

3333
import java.lang.reflect.Constructor;
34+
import java.lang.reflect.Executable;
3435
import java.lang.reflect.InvocationTargetException;
3536
import java.lang.reflect.Method;
3637
import java.math.BigInteger;
@@ -149,7 +150,12 @@ private Score deploy(Account caller, Score score, Class<?> mainClass, Object[] p
149150
// User SCORE should only have one public constructor
150151
throw new AssertionError("multiple public constructors found");
151152
}
152-
score.setInstance(ctor[0].newInstance(params));
153+
var readonly = checkAnnotationsAndReadOnly(mainClass, ctor[0], params.length, false, false, false);
154+
if (readonly) {
155+
throw new IllegalStateException("ExternalReadOnlyConstructor(class="+mainClass+")");
156+
}
157+
var params2 = convertParameters(ctor[0], params);
158+
score.setInstance(ctor[0].newInstance(params2));
153159
applyFrame();
154160
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
155161
throw e;
@@ -310,55 +316,8 @@ private Object handleCall(Account from, BigInteger value, boolean transfer, bool
310316
throw new IllegalArgumentException(
311317
"NoValidMethod(score="+score.getAddress()+",method="+method+")");
312318
}
313-
if (scoreClass.isAnnotationPresent(TScore.class)) {
314-
// reject calling internal method
315-
var external = scoreMethod.getAnnotation(TExternal.class);
316-
if (external==null) {
317-
throw new IllegalArgumentException(
318-
"NotExternal(score="+score.getAddress()+",method="+method+")"
319-
);
320-
}
321-
322-
// reject calling writable in readonly context
323-
if (isReadonly() && !external.readonly()) {
324-
throw new IllegalArgumentException(
325-
"PermissionDenied(score="+score.getAddress()+",method="+method+")"
326-
);
327-
}
328-
readonly |= external.readonly();
329-
330-
// check payable
331-
if (!external.payable() && value.signum()>0) {
332-
throw new IllegalArgumentException(
333-
"NotPayable(score="+score.getAddress()+",method="+method+")"
334-
);
335-
}
336-
337-
// optional parameter check.
338-
var parameterAnnotations = scoreMethod.getParameterAnnotations();
339-
int minParams = parameterAnnotations.length;
340-
for (int i=0 ; i<parameterAnnotations.length ; i++) {
341-
if (Arrays.stream(parameterAnnotations[i])
342-
.anyMatch((a)->(a.annotationType().equals(TOptional.class)))) {
343-
if (i < minParams) {
344-
minParams = i;
345-
} else {
346-
throw new IllegalArgumentException(
347-
"InvalidOptionalTag(score=" + score.getAddress() + ",method=" + method + ")"
348-
);
349-
}
350-
}
351-
}
352-
if (params.length < minParams) {
353-
throw new IllegalArgumentException(
354-
"NotEnoughParams(score=" + score.getAddress()
355-
+ ",method=" + method
356-
+ ",min=" + minParams
357-
+ ",given=" + params.length
358-
+ ")"
359-
);
360-
}
361-
}
319+
readonly = checkAnnotationsAndReadOnly(scoreClass, scoreMethod, params.length,
320+
true, isReadonly()|readonly, value.signum()>0);
362321
Account to = score.getAccount();
363322
pushFrame(from, to, readonly, method, value);
364323
try {
@@ -592,11 +551,55 @@ public static ServiceManager getServiceManager() {
592551
return getServiceManagerImpl();
593552
}
594553

595-
private static Object[] convertParameters(Method method, Object[] params) {
554+
private static Object defaultValueFor(Class<?> clz) {
555+
if (clz==boolean.class) {
556+
return false;
557+
} else if (clz==byte.class || clz==Byte.class) {
558+
return (byte) 0x00;
559+
} else if (clz==char.class || clz==Character.class) {
560+
return (char) 0x00;
561+
} else if (clz==short.class || clz==Short.class) {
562+
return (short) 0x00;
563+
} else if (clz==int.class || clz==Integer.class) {
564+
return 0;
565+
} else if (clz==long.class || clz==Long.class) {
566+
return 0L;
567+
} else if (clz==BigInteger.class) {
568+
return BigInteger.ZERO;
569+
}
570+
return null;
571+
}
572+
573+
private static Object[] convertParameters(Executable method, Object[] params) {
596574
Class<?>[] parameterTypes = method.getParameterTypes();
597575
int numberOfParams = parameterTypes.length;
598576
Object[] parsedParams = new Object[numberOfParams];
599577

578+
var scoreClz = method.getDeclaringClass();
579+
if (scoreClz.isAnnotationPresent(TScore.class)) {
580+
var annotations = method.getParameterAnnotations();
581+
int minParams = annotations.length;
582+
for (int i=0 ; i<annotations.length ; i++) {
583+
if (Arrays.stream(annotations[i])
584+
.anyMatch((a)->(a.annotationType().equals(TOptional.class)))) {
585+
if (i < minParams) {
586+
minParams = i;
587+
} else {
588+
throw new IllegalArgumentException(
589+
"InvalidOptionalTag(class=" + method.getDeclaringClass().getName() + ",method=" + method + ")"
590+
);
591+
}
592+
}
593+
}
594+
if (params.length < minParams) {
595+
throw new IllegalArgumentException(
596+
String.format("NotEnoughParameter(given=%d,min=%d)",
597+
params.length, minParams
598+
)
599+
);
600+
}
601+
}
602+
600603
int i = 0;
601604
for (Class<?> parameterClass : parameterTypes) {
602605
if (parameterClass == Map.class || parameterClass == List.class) {
@@ -605,7 +608,7 @@ private static Object[] convertParameters(Method method, Object[] params) {
605608
i, parameterClass.getName()));
606609
}
607610
if (i>=params.length) {
608-
parsedParams[i] = null;
611+
parsedParams[i] = defaultValueFor(parameterClass);
609612
} else {
610613
try {
611614
parsedParams[i] = TypeConverter.cast(params[i], parameterClass);
@@ -620,6 +623,59 @@ private static Object[] convertParameters(Method method, Object[] params) {
620623
return parsedParams;
621624
}
622625

626+
627+
private boolean checkAnnotationsAndReadOnly(Class<?> scoreClz, Executable method, int params, boolean external, boolean readonly, boolean payable) {
628+
if (!scoreClz.isAnnotationPresent(TScore.class)) {
629+
return false;
630+
}
631+
var externalAnnotation = method.getAnnotation(TExternal.class);
632+
if (externalAnnotation==null) {
633+
if (external) {
634+
throw new IllegalArgumentException(
635+
"NotExternal(score="+scoreClz.getName()+",method="+method+")"
636+
);
637+
}
638+
} else {
639+
if (readonly && !externalAnnotation.readonly()) {
640+
throw new IllegalArgumentException(
641+
"PermissionDenied(score="+
642+
scoreClz.getName()+",method="+method+")"
643+
);
644+
}
645+
readonly |= externalAnnotation.readonly();
646+
if (payable && !externalAnnotation.payable()) {
647+
throw new IllegalArgumentException(
648+
"NotPayable(score="+
649+
scoreClz.getName()+",method="+method+")"
650+
);
651+
}
652+
}
653+
var annotations = method.getParameterAnnotations();
654+
int minParams = annotations.length;
655+
for (int i=0 ; i<annotations.length ; i++) {
656+
if (Arrays.stream(annotations[i])
657+
.anyMatch((a)->(a.annotationType().equals(TOptional.class)))) {
658+
if (i < minParams) {
659+
minParams = i;
660+
} else {
661+
throw new IllegalArgumentException(
662+
"InvalidOptionalTag(class=" + scoreClz.getName() + ",method=" + method + ")"
663+
);
664+
}
665+
}
666+
}
667+
if (params < minParams) {
668+
throw new IllegalArgumentException(
669+
String.format("NotEnoughParameter(score=%s,method=%s,given=%d,min=%d)",
670+
scoreClz.getName(),
671+
method.toString(),
672+
params, minParams
673+
)
674+
);
675+
}
676+
return readonly;
677+
}
678+
623679
private Object invokeMethod(Score score, String methodName, Object[] params) {
624680
try {
625681
var scoreObj = score.getInstance();

unittest/src/main/java/score/impl/TypeConverter.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -210,21 +210,21 @@ private static Object specialize(Object so, Class<?> cls) {
210210
if (so == null ) {
211211
return null;
212212
}
213-
if (cls == so.getClass() || cls == boolean.class) {
213+
if (cls == so.getClass() || cls == boolean.class || cls == Boolean.class) {
214214
return so;
215-
} else if (cls == byte.class) {
215+
} else if (cls == byte.class || cls == Byte.class) {
216216
var o = (BigInteger)so;
217217
return o.byteValueExact();
218-
} else if (cls == char.class) {
218+
} else if (cls == char.class || cls == Character.class) {
219219
var o = (BigInteger)so;
220220
requireCharacterRange(o);
221221
return (char)o.intValue();
222-
} else if (cls == short.class) {
222+
} else if (cls == short.class || cls == Short.class) {
223223
var o = (BigInteger)so;
224224
return o.shortValueExact();
225-
} else if (cls == int.class) {
225+
} else if (cls == int.class || cls == Integer.class) {
226226
return ((BigInteger)so).intValueExact();
227-
} else if (cls == long.class) {
227+
} else if (cls == long.class || cls == Long.class) {
228228
return ((BigInteger) so).longValueExact();
229229
} else if (cls == List.class) {
230230
var o = (Object[])so;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2024 PARAMETA Corp.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package score;
18+
19+
import com.iconloop.score.test.Account;
20+
import com.iconloop.score.test.GenerateTScore;
21+
import com.iconloop.score.test.ServiceManager;
22+
import com.iconloop.score.test.TestBase;
23+
import org.junit.jupiter.api.Test;
24+
import score.annotation.External;
25+
import score.annotation.Optional;
26+
27+
import java.util.Objects;
28+
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertThrows;
31+
32+
public class ConstructorTest extends TestBase {
33+
private static final ServiceManager sm = getServiceManager();
34+
private static final Account owner = sm.createAccount();
35+
36+
public static class DataHolder {
37+
private final int value1;
38+
private final int value2;
39+
public DataHolder(int value1, @Optional int value2) {
40+
this.value1 = value1;
41+
this.value2 = value2;
42+
}
43+
44+
@External(readonly = true)
45+
public int getValue1() {
46+
return value1;
47+
}
48+
49+
@External(readonly = true)
50+
public int getValue2() {
51+
return value2;
52+
}
53+
}
54+
55+
@GenerateTScore(DataHolder.class)
56+
@Test
57+
void deployTest() throws Exception {
58+
assertThrows(IllegalArgumentException.class, () -> {
59+
var score = sm.deploy(owner, ConstructorTestDataHolderTS.class);
60+
});
61+
62+
var score = sm.deploy(owner, ConstructorTestDataHolderTS.class, 1);
63+
var value1 = score.call(int.class, "getValue1");
64+
var value2 = score.call(int.class, "getValue2");
65+
assertEquals(1, value1);
66+
assertEquals(0, value2);
67+
68+
score = sm.deploy(owner, ConstructorTestDataHolderTS.class, 2, 3);
69+
value1 = score.call(int.class, "getValue1");
70+
value2 = score.call(int.class, "getValue2");
71+
assertEquals(2, value1);
72+
assertEquals(3, value2);
73+
}
74+
}

0 commit comments

Comments
 (0)