mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
3e1af3bd6a
Some work I did during the holidays as part of improving the performance of interpolated strings. This change is however not isolated to those, but updates the `String()` implementation of all AST node types (term values and policy components). This change also lays the groundwork for migrating OPA to the `json/v2` package once that's stable. The `json/v2` package provides low-level functions for zero alloc marshalling via appenders — and well, here they are. The appenders here should be usable for that purpose with only a few tweaks needed for the few cases where our `String()` implementations aren't also valid JSON. Creating perfectly sized buffers requires knowing the expected length beforehand. In order to do this, each component now implements not only `encoding.AppendText` but a new custom `StringLengther` interface, which allows asking any AST node about its `StringLength()` before `make`ing a buffer of that length. We could definitely consider adding these to e.g. the `Value` or `Node` interfaces, but I've left that out of this PR as it's an easy thing to do later should we want to, and I guess there's always some concerns about changing public interfaces even when they're not meant to be implemented by external code. While no `Value` appenders allocate and almost none of the policy appenders do either, one notable exception is `Module` when there are annotations present, as they are a bit of a (YAML) special case. It's doable, but as serializing full modules isn't on a hot path anywhere, I have chosen to defer that work to the future. Signed-off-by: Anders Eknert <anders.eknert@apple.com>
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
// Copyright 2025 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
package ast
|
|
|
|
import (
|
|
"encoding"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
var builtinNamesByNumParts = sync.OnceValue(func() map[int][]string {
|
|
m := map[int][]string{}
|
|
for name := range BuiltinMap {
|
|
parts := strings.Count(name, ".") + 1
|
|
if parts > 1 {
|
|
m[parts] = append(m[parts], name)
|
|
}
|
|
}
|
|
return m
|
|
})
|
|
|
|
// BuiltinNameFromRef attempts to extract a known built-in function name from a ref,
|
|
// in the most efficient way possible. I.e. without allocating memory for a new string.
|
|
// If no built-in function name can be extracted, the second return value is false.
|
|
func BuiltinNameFromRef(ref Ref) (string, bool) {
|
|
reflen := len(ref)
|
|
if reflen == 0 {
|
|
return "", false
|
|
}
|
|
|
|
_var, ok := ref[0].Value.(Var)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
varName := string(_var)
|
|
if reflen == 1 {
|
|
if _, ok := BuiltinMap[varName]; ok {
|
|
return varName, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
totalLen := len(varName)
|
|
for _, term := range ref[1:] {
|
|
if _, ok = term.Value.(String); !ok {
|
|
return "", false
|
|
}
|
|
totalLen += 1 + len(term.Value.(String)) // account for dot
|
|
}
|
|
|
|
matched, ok := builtinNamesByNumParts()[reflen]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
for _, name := range matched {
|
|
// This check saves us a huge amount of work, as only very few built-in
|
|
// names will have the exact same length as the ref we are checking.
|
|
if len(name) != totalLen {
|
|
continue
|
|
}
|
|
// Example: `name` is "io.jwt.decode" (and so is ref)
|
|
// The first part is varName, which have already been established to be 'io':
|
|
// io, jwt.decode io == io
|
|
if curr, remaining, _ := strings.Cut(name, "."); curr == varName {
|
|
// Loop over the remaining (now known to be string) terms in the ref, e.g. "jwt" and "decode"
|
|
for _, term := range ref[1:] {
|
|
ts := string(term.Value.(String))
|
|
// First iteration: jwt.decode != jwt, so we continue cutting
|
|
// Second iteration: remaining is "decode", and so is term
|
|
if remaining == ts {
|
|
return name, true
|
|
}
|
|
// Cutting remaining (e.g. jwt.decode), and we now get:
|
|
// jwt, decode, false || jwt != jwt
|
|
if curr, remaining, _ = strings.Cut(remaining, "."); remaining == "" || curr != ts {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func AppendDelimeted[T encoding.TextAppender](buf []byte, appenders []T, delim string) ([]byte, error) {
|
|
for i, item := range appenders {
|
|
if i > 0 {
|
|
buf = append(buf, delim...)
|
|
}
|
|
var err error
|
|
if buf, err = item.AppendText(buf); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return buf, nil
|
|
}
|