1717import static com .google .common .collect .ImmutableSet .toImmutableSet ;
1818import static java .lang .Math .max ;
1919import static java .lang .Math .min ;
20+ import static java .nio .charset .StandardCharsets .UTF_8 ;
2021
2122import com .google .common .base .Ascii ;
2223import com .google .common .base .Joiner ;
2324import com .google .common .base .Splitter ;
2425import com .google .common .collect .ImmutableList ;
2526import com .google .common .collect .ImmutableSet ;
27+ import com .google .common .primitives .UnsignedLong ;
2628import com .google .errorprone .annotations .Immutable ;
2729import dev .cel .checker .CelCheckerBuilder ;
2830import dev .cel .common .CelFunctionDecl ;
2931import dev .cel .common .CelOverloadDecl ;
3032import dev .cel .common .internal .CelCodePointArray ;
33+ import dev .cel .common .internal .DateTimeHelpers ;
34+ import dev .cel .common .types .CelType ;
3135import dev .cel .common .types .ListType ;
3236import dev .cel .common .types .SimpleType ;
37+ import dev .cel .common .types .TypeType ;
38+ import dev .cel .common .values .CelByteString ;
39+ import dev .cel .common .values .NullValue ;
3340import dev .cel .compiler .CelCompilerLibrary ;
3441import dev .cel .runtime .CelEvaluationException ;
3542import dev .cel .runtime .CelEvaluationExceptionBuilder ;
3643import dev .cel .runtime .CelFunctionBinding ;
3744import dev .cel .runtime .CelRuntimeBuilder ;
3845import dev .cel .runtime .CelRuntimeLibrary ;
46+ import java .math .BigDecimal ;
47+ import java .math .RoundingMode ;
48+ import java .time .Duration ;
49+ import java .time .Instant ;
50+ import java .util .HexFormat ;
3951import java .util .List ;
52+ import java .util .Locale ;
53+ import java .util .Map ;
4054import java .util .Set ;
55+ import java .util .TreeMap ;
4156
4257/** Internal implementation of CEL string extensions. */
4358@ Immutable
@@ -58,6 +73,16 @@ public enum Function {
5873 ImmutableList .of (SimpleType .STRING , SimpleType .INT ))),
5974 CelFunctionBinding .from (
6075 "string_char_at_int" , String .class , Long .class , CelStringExtensions ::charAt )),
76+ FORMAT (
77+ CelFunctionDecl .newFunctionDeclaration (
78+ "format" ,
79+ CelOverloadDecl .newMemberOverload (
80+ "string_format" ,
81+ "Formats the string using the provided arguments." ,
82+ SimpleType .STRING ,
83+ ImmutableList .of (SimpleType .STRING , ListType .create (SimpleType .DYN )))),
84+ CelFunctionBinding .from (
85+ "string_format" , String .class , List .class , CelStringExtensions ::format )),
6186 INDEX_OF (
6287 CelFunctionDecl .newFunctionDeclaration (
6388 "indexOf" ,
@@ -404,6 +429,272 @@ private static String join(List<String> stringList, String separator) {
404429 return Joiner .on (separator ).join (stringList );
405430 }
406431
432+ private static String format (String formatSpecifier , List <Object > args )
433+ throws CelEvaluationException {
434+ StringBuilder builtStr = new StringBuilder (formatSpecifier .length ());
435+ int i = 0 ;
436+ int argIndex = 0 ;
437+ while (i < formatSpecifier .length ()) {
438+ if (formatSpecifier .charAt (i ) == '%' ) {
439+ if (i + 1 < formatSpecifier .length () && formatSpecifier .charAt (i + 1 ) == '%' ) {
440+ builtStr .append ('%' );
441+ i += 2 ;
442+ } else {
443+ if (argIndex >= args .size ()) {
444+ throw new CelEvaluationException ("index " + argIndex + " out of range" );
445+ }
446+ Object arg = args .get (argIndex ++);
447+ i ++; // Skip '%'
448+
449+ int precision = -1 ;
450+ if (i < formatSpecifier .length () && formatSpecifier .charAt (i ) == '.' ) {
451+ i ++;
452+ int start = i ;
453+ while (i < formatSpecifier .length () && Character .isDigit (formatSpecifier .charAt (i ))) {
454+ i ++;
455+ }
456+ if (i == start ) {
457+ throw new CelEvaluationException ("could not find end of precision specifier" );
458+ }
459+ try {
460+ precision = Integer .parseInt (formatSpecifier .substring (start , i ));
461+ } catch (NumberFormatException e ) {
462+ throw new CelEvaluationException ("error while converting precision to integer" , e );
463+ }
464+ }
465+
466+ if (i >= formatSpecifier .length ()) {
467+ throw new CelEvaluationException ("unexpected end of string" );
468+ }
469+ char verb = formatSpecifier .charAt (i ++);
470+
471+ switch (verb ) {
472+ case 's' -> builtStr .append (formatString (arg ));
473+ case 'd' -> builtStr .append (formatDecimal (arg ));
474+ case 'f' -> builtStr .append (formatFixed (arg , precision ));
475+ case 'e' -> builtStr .append (formatScientific (arg , precision ));
476+ case 'b' -> builtStr .append (formatBinary (arg ));
477+ case 'x' , 'X' -> builtStr .append (formatHex (arg , verb == 'X' ));
478+ case 'o' -> builtStr .append (formatOctal (arg ));
479+ default ->
480+ throw new CelEvaluationException ("unrecognized formatting clause \" " + verb + "\" " );
481+ }
482+ }
483+ } else {
484+ builtStr .append (formatSpecifier .charAt (i ++));
485+ }
486+ }
487+ return builtStr .toString ();
488+ }
489+
490+ private static String formatString (Object val ) throws CelEvaluationException {
491+ if (val == null ) {
492+ return "null" ;
493+ }
494+ if (val instanceof String s ) {
495+ return s ;
496+ }
497+ if (val instanceof CelByteString byteString ) {
498+ return byteString .toStringUtf8 ();
499+ }
500+ if (val instanceof Duration duration ) {
501+ return DateTimeHelpers .toString (duration );
502+ }
503+ if (val instanceof Instant ) {
504+ return val .toString ();
505+ }
506+ if (val instanceof Boolean ) {
507+ return val .toString ();
508+ }
509+ if (val instanceof Long ) {
510+ return val .toString ();
511+ }
512+ if (val instanceof UnsignedLong ) {
513+ return val .toString ();
514+ }
515+ if (val instanceof Double d ) {
516+ if (d .isNaN ()) {
517+ return "NaN" ;
518+ }
519+ if (d .isInfinite ()) {
520+ return d > 0 ? "Infinity" : "-Infinity" ;
521+ }
522+ return d .toString ();
523+ }
524+ if (val instanceof List <?> list ) {
525+ return formatList (list );
526+ }
527+ if (val instanceof Map <?, ?> map ) {
528+ return formatMap (map );
529+ }
530+ if (val instanceof NullValue ) {
531+ return "null" ;
532+ }
533+ if (val instanceof TypeType typeType ) {
534+ return typeType .containingTypeName ();
535+ }
536+ if (val instanceof CelType celType ) {
537+ return celType .name ();
538+ }
539+ throw new CelEvaluationException (
540+ "could not convert argument " + val .getClass ().getName () + " to string" );
541+ }
542+
543+ private static String formatList (List <?> list ) throws CelEvaluationException {
544+ StringBuilder sb = new StringBuilder ("[" );
545+ for (int i = 0 ; i < list .size (); i ++) {
546+ sb .append (formatString (list .get (i )));
547+ if (i < list .size () - 1 ) {
548+ sb .append (", " );
549+ }
550+ }
551+ sb .append ("]" );
552+ return sb .toString ();
553+ }
554+
555+ private static String formatMap (Map <?, ?> map ) throws CelEvaluationException {
556+ TreeMap <String , Object > sortedMap = new TreeMap <>();
557+ for (Map .Entry <?, ?> entry : map .entrySet ()) {
558+ String keyStr = formatString (entry .getKey ());
559+ sortedMap .put (keyStr , entry .getValue ());
560+ }
561+ StringBuilder sb = new StringBuilder ("{" );
562+ int i = 0 ;
563+ for (Map .Entry <String , Object > entry : sortedMap .entrySet ()) {
564+ sb .append (entry .getKey ()).append (": " ).append (formatString (entry .getValue ()));
565+ if (i < sortedMap .size () - 1 ) {
566+ sb .append (", " );
567+ }
568+ i ++;
569+ }
570+ sb .append ("}" );
571+ return sb .toString ();
572+ }
573+
574+ private static String formatDecimal (Object arg ) throws CelEvaluationException {
575+ if (arg instanceof Long || arg instanceof UnsignedLong ) {
576+ return arg .toString ();
577+ }
578+ if (arg instanceof Double ) {
579+ return formatFixed (arg , -1 );
580+ }
581+ throw new CelEvaluationException (
582+ "decimal clause can only be used on numbers, was given " + arg .getClass ().getName ());
583+ }
584+
585+ private static String formatFixed (Object arg , int precision ) throws CelEvaluationException {
586+ if (arg instanceof Double d ) {
587+ double val = d ;
588+ if (Double .isNaN (val )) {
589+ return "NaN" ;
590+ }
591+ if (Double .isInfinite (val )) {
592+ return val > 0 ? "Infinity" : "-Infinity" ;
593+ }
594+ int p = precision >= 0 ? precision : 6 ;
595+ BigDecimal bd = BigDecimal .valueOf (val );
596+ bd = bd .setScale (p , RoundingMode .HALF_EVEN );
597+ return bd .toPlainString ();
598+ }
599+ if (arg instanceof Long l ) {
600+ return formatFixed ((double ) l , precision );
601+ }
602+ if (arg instanceof UnsignedLong ulong ) {
603+ return formatFixed (ulong .doubleValue (), precision );
604+ }
605+ throw new CelEvaluationException (
606+ "fixed point clause can only be used on doubles, integers, and unsigned integers, was given"
607+ + " "
608+ + arg .getClass ().getName ());
609+ }
610+
611+ private static String formatScientific (Object arg , int precision ) throws CelEvaluationException {
612+ if (arg instanceof Double d ) {
613+ double val = d ;
614+ if (Double .isNaN (val )) {
615+ return "NaN" ;
616+ }
617+ if (Double .isInfinite (val )) {
618+ return val > 0 ? "Infinity" : "-Infinity" ;
619+ }
620+ String fmtStr = precision >= 0 ? "%." + precision + "e" : "%.6e" ;
621+ return String .format (Locale .ROOT , fmtStr , val );
622+ }
623+ if (arg instanceof Long l ) {
624+ return formatScientific ((double ) l , precision );
625+ }
626+ if (arg instanceof UnsignedLong ulong ) {
627+ return formatScientific (ulong .doubleValue (), precision );
628+ }
629+ throw new CelEvaluationException (
630+ "scientific clause can only be used on doubles, integers, and unsigned integers, was given "
631+ + arg .getClass ().getName ());
632+ }
633+
634+ private static String formatBinary (Object arg ) throws CelEvaluationException {
635+ if (arg instanceof Long val ) {
636+ if (val < 0 ) {
637+ if (val == Long .MIN_VALUE ) {
638+ return "-1" + "0" .repeat (63 );
639+ }
640+ return "-" + Long .toBinaryString (-val );
641+ }
642+ return Long .toBinaryString (val );
643+ }
644+ if (arg instanceof UnsignedLong ulong ) {
645+ return ulong .toString (2 );
646+ }
647+ if (arg instanceof Boolean b ) {
648+ return b ? "1" : "0" ;
649+ }
650+ throw new CelEvaluationException (
651+ "binary clause can only be used on integers and bools, was given "
652+ + arg .getClass ().getName ());
653+ }
654+
655+ private static String formatHex (Object arg , boolean upper ) throws CelEvaluationException {
656+ String result ;
657+ if (arg instanceof Long val ) {
658+ if (val < 0 ) {
659+ if (val == Long .MIN_VALUE ) {
660+ result = "-8000000000000000" ;
661+ } else {
662+ result = "-" + String .format ("%x" , -val );
663+ }
664+ } else {
665+ result = String .format ("%x" , val );
666+ }
667+ } else if (arg instanceof UnsignedLong unsignedLong ) {
668+ result = unsignedLong .toString (16 );
669+ } else if (arg instanceof CelByteString byteString ) {
670+ result = HexFormat .of ().formatHex (byteString .toByteArray ());
671+ } else if (arg instanceof String str ) {
672+ result = HexFormat .of ().formatHex (str .getBytes (UTF_8 ));
673+ } else {
674+ throw new CelEvaluationException (
675+ "hex clause can only be used on integers, byte buffers, and strings, was given "
676+ + arg .getClass ().getName ());
677+ }
678+ return upper ? result .toUpperCase (Locale .ROOT ) : result ;
679+ }
680+
681+ private static String formatOctal (Object arg ) throws CelEvaluationException {
682+ if (arg instanceof Long val ) {
683+ if (val < 0 ) {
684+ if (val == Long .MIN_VALUE ) {
685+ return "-1000000000000000000000" ;
686+ }
687+ return "-" + String .format ("%o" , -val );
688+ }
689+ return String .format ("%o" , val );
690+ }
691+ if (arg instanceof UnsignedLong ulong ) {
692+ return ulong .toString (8 );
693+ }
694+ throw new CelEvaluationException (
695+ "octal clause can only be used on integers, was given " + arg .getClass ().getName ());
696+ }
697+
407698 private static Long lastIndexOf (String str , String substr ) throws CelEvaluationException {
408699 CelCodePointArray strCpa = CelCodePointArray .fromString (str );
409700 CelCodePointArray substrCpa = CelCodePointArray .fromString (substr );
0 commit comments