├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── ast ├── ast.go └── walk.go ├── cmd └── riverfmt │ └── main.go ├── diag ├── diag.go ├── printer.go └── printer_test.go ├── encoding └── riverjson │ ├── riverjson.go │ ├── riverjson_test.go │ └── types.go ├── go.mod ├── go.sum ├── internal ├── reflectutil │ ├── walk.go │ └── walk_test.go ├── rivertags │ ├── rivertags.go │ └── rivertags_test.go ├── stdlib │ ├── constants.go │ └── stdlib.go └── value │ ├── capsule.go │ ├── decode.go │ ├── decode_benchmarks_test.go │ ├── decode_test.go │ ├── errors.go │ ├── number_value.go │ ├── raw_function.go │ ├── tag_cache.go │ ├── type.go │ ├── type_test.go │ ├── value.go │ ├── value_object.go │ ├── value_object_test.go │ └── value_test.go ├── parser ├── error_test.go ├── internal.go ├── internal_test.go ├── parser.go ├── parser_test.go └── testdata │ ├── assign_block_to_attr.river │ ├── attribute_names.river │ ├── block_names.river │ ├── commas.river │ ├── fuzz │ └── FuzzParser │ │ ├── 1a39f4e358facc21678b16fad53537b46efdaa76e024a5ef4955d01a68bdac37 │ │ ├── 248cf4391f6c48550b7d2cf4c6c80f4ba9099c21ffa2b6869e75e99565dce037 │ │ └── b919fa00ebca318001778477c839a06204b55f2636597901d8d7878150d8580a │ ├── invalid_exprs.river │ ├── invalid_object_key.river │ └── valid │ ├── attribute.river │ ├── blocks.river │ ├── comments.river │ ├── empty.river │ └── expressions.river ├── printer ├── printer.go ├── printer_test.go ├── testdata │ ├── .gitattributes │ ├── array_comments.expect │ ├── array_comments.in │ ├── block_comments.expect │ ├── block_comments.in │ ├── example.expect │ ├── example.in │ ├── func_call.expect │ ├── func_call.in │ ├── mixed_list.expect │ ├── mixed_list.in │ ├── mixed_object.expect │ ├── mixed_object.in │ ├── object_align.expect │ ├── object_align.in │ ├── oneline_block.expect │ ├── oneline_block.in │ ├── raw_string.expect │ ├── raw_string.in │ ├── raw_string_label_error.error │ └── raw_string_label_error.in ├── trimmer.go └── walker.go ├── river.go ├── river_test.go ├── rivertypes ├── optional_secret.go ├── optional_secret_test.go ├── secret.go └── secret_test.go ├── scanner ├── identifier.go ├── identifier_test.go ├── scanner.go └── scanner_test.go ├── token ├── builder │ ├── builder.go │ ├── builder_test.go │ ├── nested_defaults_test.go │ ├── token.go │ └── value_tokens.go ├── file.go └── token.go ├── types.go └── vm ├── constant.go ├── error.go ├── op_binary.go ├── op_binary_test.go ├── op_unary.go ├── struct_decoder.go ├── tag_cache.go ├── vm.go ├── vm_benchmarks_test.go ├── vm_block_test.go ├── vm_errors_test.go ├── vm_stdlib_test.go └── vm_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | open-pull-requests-limit: 2 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - run: git fetch --force --tags 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.21 22 | - uses: goreleaser/goreleaser-action@v5 23 | with: 24 | version: latest 25 | args: release --clean 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | lint: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Set up Go 1.21 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.21 18 | - name: Install golangci-lint 19 | run: | 20 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/local/bin v1.54.2 21 | - name: Lint 22 | run: make lint 23 | test: 24 | name: Test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | - name: Set up Go 1.21 30 | uses: actions/setup-go@v4 31 | with: 32 | go-version: 1.21 33 | - name: Test 34 | run: make test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Full list of configuration options: https://golangci-lint.run/usage/configuration/ 2 | 3 | run: 4 | timeout: 10m 5 | 6 | output: 7 | sort-results: true 8 | 9 | linters: 10 | enable: 11 | - errcheck # Report unchecked errors 12 | - goconst # Find repeated strings that could be replaced by constant 13 | - gofmt # Check whether code was gofmt-ed 14 | - goimports # Check imports were formatted with gofmt 15 | - revive # Broad set of rules; replaces deprecated golint 16 | - gosimple # Check whether code can be simplified 17 | - ineffassign # Detect when assignment to variable is never used 18 | - misspell # Report on commonly misspelled English words 19 | - unconvert # Remove unnecessary type conversions 20 | - unparam # Detect unused function parameters 21 | - govet # `go vet` 22 | - unused # Detect unused constants/variables/functions/types 23 | - typecheck # Ensure code typechecks 24 | - makezero # Detect misuse of make with non-zero length and append 25 | - tenv # Use testing.(*T).Setenv instead of os.Setenv 26 | - whitespace # Report unnecessary blank lines 27 | 28 | issues: 29 | # We want to use our own exclusion rules and ignore all the defaults. 30 | exclude-use-default: false 31 | 32 | exclude-rules: 33 | # It's fine if tests ignore errors. 34 | - path: _test.go 35 | linters: 36 | - errcheck 37 | 38 | exclude: 39 | # Ignoring errors on Close, Log, and removing files is OK in most cases. 40 | - "Error return value of `(.*\\.Close|.*\\.Log|os.Remove)` is not checked" 41 | 42 | # Linter settings options: https://golangci-lint.run/usage/linters/ 43 | linters-settings: 44 | whitespace: 45 | # While there normally shouldn't be extra redundant leading/trailing 46 | # whitespace, if statement conditions and function headers that cross 47 | # multiple lines are an exception. 48 | # 49 | # if true || 50 | # false { 51 | # 52 | # // ... ^ must have empty line above 53 | # } 54 | # 55 | # func foo( 56 | # a int, 57 | # ) { 58 | # 59 | # // ... ^ must have empty line above 60 | # } 61 | # 62 | # This helps readers easily separate where the multi-line if/function ends 63 | # at a glance. 64 | multi-if: true 65 | multi-func: true 66 | 67 | revive: 68 | rules: 69 | - name: package-comments 70 | disabled: true 71 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | release: 2 | draft: true 3 | replace_existing_draft: true 4 | 5 | builds: 6 | - id: riverfmt 7 | main: ./cmd/riverfmt/ 8 | binary: riverfmt 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | goarch: 16 | - amd64 17 | - arm 18 | - arm64 19 | goarm: 20 | - 6 21 | - 7 22 | ignore: 23 | - goos: windows 24 | goarch: arm 25 | 26 | archives: 27 | - id: riverfmt-archive 28 | format: tar.gz 29 | name_template: >- 30 | riverfmt_ 31 | {{- .Os }}_ 32 | {{- .Arch }} 33 | {{- if .Arm }}v{{ .Arm }}{{ end }} 34 | builds: 35 | - riverfmt 36 | files: ['NONE*'] 37 | format_overrides: 38 | - goos: windows 39 | format: zip 40 | 41 | checksum: 42 | name_template: 'SHA256SUMS' 43 | algorithm: sha256 44 | 45 | changelog: 46 | sort: asc 47 | filters: 48 | exclude: 49 | - '^docs:' 50 | - '^test:' 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This document contains a historical list of changes between releases. Only 4 | changes that impact end-user behavior are listed; changes to documentation or 5 | internal API changes are not present. 6 | 7 | Main (unreleased) 8 | ----------------- 9 | 10 | ### Features 11 | 12 | - Add support for raw map[string]any type to riverjson encoding. (@wildum) 13 | 14 | v0.3.0 (2023-10-26) 15 | ------------------- 16 | 17 | ### Enhancements 18 | 19 | - Allow the `[]` operator to return `null` when accessing non-existant keys in 20 | objects, rather than returning an error. For example, `{}["foo"]` now returns 21 | `null`. (@rfratto) 22 | 23 | ### Bugfixes 24 | 25 | - Fix a bug where indexing an object with a non-string key would generate a 26 | type error informing users to supply a `number` instead of the actually 27 | expected `string` type. (@rfratto) 28 | 29 | v0.2.0 (2023-10-20) 30 | ------------------- 31 | 32 | ### Features 33 | 34 | - Add support for raw strings in river using backticks (@erikbaranowski) 35 | 36 | - Add functions for validating and sanitizing River identifiers to the scanner package (@erikbaranowski) 37 | 38 | v0.1.1 (2023-08-25) 39 | ------------------- 40 | 41 | ### Other changes 42 | 43 | - Fix typos and expand README for documentation. 44 | 45 | - `token/builder`: Update River encode handling of optional fields to compare values using 46 | DeepEqual even if they don't implement Equal when deciding if the field 47 | should be included. (@erikbaranowski) 48 | 49 | v0.1.0 (2023-08-25) 50 | ------------------- 51 | 52 | > First release! 53 | 54 | ### Features 55 | 56 | - Publish a `riverfmt` binary for formatting River files. 57 | 58 | - Publish River as a library: 59 | 60 | - `github.com/grafana/river/ast` contains the AST representation of River with some utilities. 61 | - `github.com/grafana/river/diag` contains types for River diagnostics (errors and warnings). 62 | - `github.com/grafana/river/encoding/riverjson` contains utilities to print River bodies as JSON. 63 | - `github.com/grafana/river/parser` contains utilities to parse River files. 64 | - `github.com/grafana/river/printer` contains utilities to format River files. 65 | - `github.com/grafana/river/rivertypes` contains useful capsule values. 66 | - `github.com/grafana/river/scanner` contains utilities to scan River files. 67 | - `github.com/grafana/river/token` contains token definitions for River. 68 | - `github.com/grafana/river/token/builder` contains utilities to build River files from Go code. 69 | - `github.com/grafana/river/vm` evalutes River blocks and expressions 70 | 71 | The top-level `github.com/grafana/river` module contains a high-level API for 72 | unmarshaling River files to Go types and marshaling Go types to River files. 73 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -v ./... 3 | 4 | lint: 5 | golangci-lint run -v 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Deprecation notice**: River has been moved to the [Grafana Alloy 2 | > repository][syntax] under the "syntax" submodule, and is now called the 3 | > "Grafana Alloy configuration syntax." 4 | > 5 | > For more information, read our blog posts about Alloy and how to easily 6 | > migrate from Agent to Alloy: 7 | > 8 | > * [Alloy announcement blog post](https://grafana.com/blog/2024/04/09/grafana-alloy-opentelemetry-collector-with-prometheus-pipelines/) 9 | > * [Alloy FAQ](https://grafana.com/blog/2024/04/09/grafana-agent-to-grafana-alloy-opentelemetry-collector-faq/) 10 | > * [Migrate to Alloy](https://grafana.com/docs/alloy/latest/tasks/migrate/) 11 | > 12 | > [syntax]: https://github.com/grafana/alloy/tree/main/syntax 13 | 14 | # River 15 | 16 | [![Go Reference](https://pkg.go.dev/badge/github.com/grafana/river.svg)](https://pkg.go.dev/github.com/grafana/river) 17 | 18 | River is an HCL-inspired configuration language originally written for 19 | [Grafana Agent flow mode][flow] with the following goals: 20 | 21 | * _Fast_: River is intended to be used in applications that may evaluate River 22 | expression multiple times a second. 23 | * _Simple_: River must be easy to read and write to minimize the learning 24 | curve of yet another configuration language. 25 | * _Debuggable_: River must give detailed information when there's a mistake in 26 | configuration. 27 | 28 | ```river 29 | // Discover Kubernetes pods to collect metrics from. 30 | discovery.kubernetes "pods" { 31 | role = "pod" 32 | } 33 | 34 | // Collect metrics from Kubernetes pods. 35 | prometheus.scrape "default" { 36 | targets = discovery.kubernetes.pods.targets 37 | forward_to = [prometheus.remote_write.default.receiver] 38 | } 39 | 40 | // Get an API key from disk. 41 | local.file "apikey" { 42 | filename = "/var/data/my-api-key.txt" 43 | is_secret = true 44 | } 45 | 46 | // Send metrics to a Prometheus remote_write endpoint. 47 | prometheus.remote_write "default" { 48 | endpoint { 49 | url = "http://localhost:9009/api/prom/push" 50 | 51 | basic_auth { 52 | username = "MY_USERNAME" 53 | password = local.file.apikey.content 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | 60 | For more information on how to use River, see [our Go documentation][docs]. 61 | 62 | [flow]: https://grafana.com/docs/agent/latest/flow 63 | [docs]: https://pkg.go.dev/github.com/grafana/river 64 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | // Package ast exposes AST elements used by River. 2 | // 3 | // The various interfaces exposed by ast are all closed; only types within this 4 | // package can satisfy an AST interface. 5 | package ast 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/grafana/river/token" 13 | ) 14 | 15 | // Node represents any node in the AST. 16 | type Node interface { 17 | astNode() 18 | } 19 | 20 | // Stmt is a type of statement within the body of a file or block. 21 | type Stmt interface { 22 | Node 23 | astStmt() 24 | } 25 | 26 | // Expr is an expression within the AST. 27 | type Expr interface { 28 | Node 29 | astExpr() 30 | } 31 | 32 | // File is a parsed file. 33 | type File struct { 34 | Name string // Filename provided to parser 35 | Body Body // Content of File 36 | Comments []CommentGroup // List of all comments in the File 37 | } 38 | 39 | // Body is a list of statements. 40 | type Body []Stmt 41 | 42 | // A CommentGroup represents a sequence of comments that are not separated by 43 | // any empty lines or other non-comment tokens. 44 | type CommentGroup []*Comment 45 | 46 | // A Comment represents a single line or block comment. 47 | // 48 | // The Text field contains the comment text without any carriage returns (\r) 49 | // that may have been present in the source. Since carriage returns get 50 | // removed, EndPos will not be accurate for any comment which contained 51 | // carriage returns. 52 | type Comment struct { 53 | StartPos token.Pos // Starting position of comment 54 | // Text of the comment. Text will not contain '\n' for line comments. 55 | Text string 56 | } 57 | 58 | // AttributeStmt is a key-value pair being set in a Body or BlockStmt. 59 | type AttributeStmt struct { 60 | Name *Ident 61 | Value Expr 62 | } 63 | 64 | // BlockStmt declares a block. 65 | type BlockStmt struct { 66 | Name []string 67 | NamePos token.Pos 68 | Label string 69 | LabelPos token.Pos 70 | Body Body 71 | 72 | LCurlyPos, RCurlyPos token.Pos 73 | } 74 | 75 | // Ident holds an identifier with its position. 76 | type Ident struct { 77 | Name string 78 | NamePos token.Pos 79 | } 80 | 81 | // IdentifierExpr refers to a named value. 82 | type IdentifierExpr struct { 83 | Ident *Ident 84 | } 85 | 86 | // LiteralExpr is a constant value of a specific token kind. 87 | type LiteralExpr struct { 88 | Kind token.Token 89 | ValuePos token.Pos 90 | 91 | // Value holds the unparsed literal value. For example, if Kind == 92 | // token.STRING, then Value would be wrapped in the original quotes (e.g., 93 | // `"foobar"`). 94 | Value string 95 | } 96 | 97 | // ArrayExpr is an array of values. 98 | type ArrayExpr struct { 99 | Elements []Expr 100 | LBrackPos, RBrackPos token.Pos 101 | } 102 | 103 | // ObjectExpr declares an object of key-value pairs. 104 | type ObjectExpr struct { 105 | Fields []*ObjectField 106 | LCurlyPos, RCurlyPos token.Pos 107 | } 108 | 109 | // ObjectField defines an individual key-value pair within an object. 110 | // ObjectField does not implement Node. 111 | type ObjectField struct { 112 | Name *Ident 113 | Quoted bool // True if the name was wrapped in quotes 114 | Value Expr 115 | } 116 | 117 | // AccessExpr accesses a field in an object value by name. 118 | type AccessExpr struct { 119 | Value Expr 120 | Name *Ident 121 | } 122 | 123 | // IndexExpr accesses an index in an array value. 124 | type IndexExpr struct { 125 | Value, Index Expr 126 | LBrackPos, RBrackPos token.Pos 127 | } 128 | 129 | // CallExpr invokes a function value with a set of arguments. 130 | type CallExpr struct { 131 | Value Expr 132 | Args []Expr 133 | 134 | LParenPos, RParenPos token.Pos 135 | } 136 | 137 | // UnaryExpr performs a unary operation on a single value. 138 | type UnaryExpr struct { 139 | Kind token.Token 140 | KindPos token.Pos 141 | Value Expr 142 | } 143 | 144 | // BinaryExpr performs a binary operation against two values. 145 | type BinaryExpr struct { 146 | Kind token.Token 147 | KindPos token.Pos 148 | Left, Right Expr 149 | } 150 | 151 | // ParenExpr represents an expression wrapped in parentheses. 152 | type ParenExpr struct { 153 | Inner Expr 154 | LParenPos, RParenPos token.Pos 155 | } 156 | 157 | // Type assertions 158 | 159 | var ( 160 | _ Node = (*File)(nil) 161 | _ Node = (*Body)(nil) 162 | _ Node = (*AttributeStmt)(nil) 163 | _ Node = (*BlockStmt)(nil) 164 | _ Node = (*Ident)(nil) 165 | _ Node = (*IdentifierExpr)(nil) 166 | _ Node = (*LiteralExpr)(nil) 167 | _ Node = (*ArrayExpr)(nil) 168 | _ Node = (*ObjectExpr)(nil) 169 | _ Node = (*AccessExpr)(nil) 170 | _ Node = (*IndexExpr)(nil) 171 | _ Node = (*CallExpr)(nil) 172 | _ Node = (*UnaryExpr)(nil) 173 | _ Node = (*BinaryExpr)(nil) 174 | _ Node = (*ParenExpr)(nil) 175 | 176 | _ Stmt = (*AttributeStmt)(nil) 177 | _ Stmt = (*BlockStmt)(nil) 178 | 179 | _ Expr = (*IdentifierExpr)(nil) 180 | _ Expr = (*LiteralExpr)(nil) 181 | _ Expr = (*ArrayExpr)(nil) 182 | _ Expr = (*ObjectExpr)(nil) 183 | _ Expr = (*AccessExpr)(nil) 184 | _ Expr = (*IndexExpr)(nil) 185 | _ Expr = (*CallExpr)(nil) 186 | _ Expr = (*UnaryExpr)(nil) 187 | _ Expr = (*BinaryExpr)(nil) 188 | _ Expr = (*ParenExpr)(nil) 189 | ) 190 | 191 | func (n *File) astNode() {} 192 | func (n Body) astNode() {} 193 | func (n CommentGroup) astNode() {} 194 | func (n *Comment) astNode() {} 195 | func (n *AttributeStmt) astNode() {} 196 | func (n *BlockStmt) astNode() {} 197 | func (n *Ident) astNode() {} 198 | func (n *IdentifierExpr) astNode() {} 199 | func (n *LiteralExpr) astNode() {} 200 | func (n *ArrayExpr) astNode() {} 201 | func (n *ObjectExpr) astNode() {} 202 | func (n *AccessExpr) astNode() {} 203 | func (n *IndexExpr) astNode() {} 204 | func (n *CallExpr) astNode() {} 205 | func (n *UnaryExpr) astNode() {} 206 | func (n *BinaryExpr) astNode() {} 207 | func (n *ParenExpr) astNode() {} 208 | 209 | func (n *AttributeStmt) astStmt() {} 210 | func (n *BlockStmt) astStmt() {} 211 | 212 | func (n *IdentifierExpr) astExpr() {} 213 | func (n *LiteralExpr) astExpr() {} 214 | func (n *ArrayExpr) astExpr() {} 215 | func (n *ObjectExpr) astExpr() {} 216 | func (n *AccessExpr) astExpr() {} 217 | func (n *IndexExpr) astExpr() {} 218 | func (n *CallExpr) astExpr() {} 219 | func (n *UnaryExpr) astExpr() {} 220 | func (n *BinaryExpr) astExpr() {} 221 | func (n *ParenExpr) astExpr() {} 222 | 223 | // StartPos returns the position of the first character belonging to a Node. 224 | func StartPos(n Node) token.Pos { 225 | if n == nil || reflect.ValueOf(n).IsZero() { 226 | return token.NoPos 227 | } 228 | switch n := n.(type) { 229 | case *File: 230 | return StartPos(n.Body) 231 | case Body: 232 | if len(n) == 0 { 233 | return token.NoPos 234 | } 235 | return StartPos(n[0]) 236 | case CommentGroup: 237 | if len(n) == 0 { 238 | return token.NoPos 239 | } 240 | return StartPos(n[0]) 241 | case *Comment: 242 | return n.StartPos 243 | case *AttributeStmt: 244 | return StartPos(n.Name) 245 | case *BlockStmt: 246 | return n.NamePos 247 | case *Ident: 248 | return n.NamePos 249 | case *IdentifierExpr: 250 | return StartPos(n.Ident) 251 | case *LiteralExpr: 252 | return n.ValuePos 253 | case *ArrayExpr: 254 | return n.LBrackPos 255 | case *ObjectExpr: 256 | return n.LCurlyPos 257 | case *AccessExpr: 258 | return StartPos(n.Value) 259 | case *IndexExpr: 260 | return StartPos(n.Value) 261 | case *CallExpr: 262 | return StartPos(n.Value) 263 | case *UnaryExpr: 264 | return n.KindPos 265 | case *BinaryExpr: 266 | return StartPos(n.Left) 267 | case *ParenExpr: 268 | return n.LParenPos 269 | default: 270 | panic(fmt.Sprintf("Unhandled Node type %T", n)) 271 | } 272 | } 273 | 274 | // EndPos returns the position of the final character in a Node. 275 | func EndPos(n Node) token.Pos { 276 | if n == nil || reflect.ValueOf(n).IsZero() { 277 | return token.NoPos 278 | } 279 | switch n := n.(type) { 280 | case *File: 281 | return EndPos(n.Body) 282 | case Body: 283 | if len(n) == 0 { 284 | return token.NoPos 285 | } 286 | return EndPos(n[len(n)-1]) 287 | case CommentGroup: 288 | if len(n) == 0 { 289 | return token.NoPos 290 | } 291 | return EndPos(n[len(n)-1]) 292 | case *Comment: 293 | return n.StartPos.Add(len(n.Text) - 1) 294 | case *AttributeStmt: 295 | return EndPos(n.Value) 296 | case *BlockStmt: 297 | return n.RCurlyPos 298 | case *Ident: 299 | return n.NamePos.Add(len(n.Name) - 1) 300 | case *IdentifierExpr: 301 | return EndPos(n.Ident) 302 | case *LiteralExpr: 303 | return n.ValuePos.Add(len(n.Value) - 1) 304 | case *ArrayExpr: 305 | return n.RBrackPos 306 | case *ObjectExpr: 307 | return n.RCurlyPos 308 | case *AccessExpr: 309 | return EndPos(n.Name) 310 | case *IndexExpr: 311 | return n.RBrackPos 312 | case *CallExpr: 313 | return n.RParenPos 314 | case *UnaryExpr: 315 | return EndPos(n.Value) 316 | case *BinaryExpr: 317 | return EndPos(n.Right) 318 | case *ParenExpr: 319 | return n.RParenPos 320 | default: 321 | panic(fmt.Sprintf("Unhandled Node type %T", n)) 322 | } 323 | } 324 | 325 | // GetBlockName retrieves the "." delimited block name. 326 | func (block *BlockStmt) GetBlockName() string { 327 | return strings.Join(block.Name, ".") 328 | } 329 | -------------------------------------------------------------------------------- /ast/walk.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "fmt" 4 | 5 | // A Visitor has its Visit method invoked for each node encountered by Walk. If 6 | // the resulting visitor w is not nil, Walk visits each of the children of node 7 | // with the visitor w, followed by a call of w.Visit(nil). 8 | type Visitor interface { 9 | Visit(node Node) (w Visitor) 10 | } 11 | 12 | // Walk traverses an AST in depth-first order: it starts by calling 13 | // v.Visit(node); node must not be nil. If the visitor w returned by 14 | // v.Visit(node) is not nil, Walk is invoked recursively with visitor w for 15 | // each of the non-nil children of node, followed by a call of w.Visit(nil). 16 | func Walk(v Visitor, node Node) { 17 | if v = v.Visit(node); v == nil { 18 | return 19 | } 20 | 21 | // Walk children. The order of the cases matches the declared order of nodes 22 | // in ast.go. 23 | switch n := node.(type) { 24 | case *File: 25 | Walk(v, n.Body) 26 | case Body: 27 | for _, s := range n { 28 | Walk(v, s) 29 | } 30 | case *AttributeStmt: 31 | Walk(v, n.Name) 32 | Walk(v, n.Value) 33 | case *BlockStmt: 34 | Walk(v, n.Body) 35 | case *Ident: 36 | // Nothing to do 37 | case *IdentifierExpr: 38 | Walk(v, n.Ident) 39 | case *LiteralExpr: 40 | // Nothing to do 41 | case *ArrayExpr: 42 | for _, e := range n.Elements { 43 | Walk(v, e) 44 | } 45 | case *ObjectExpr: 46 | for _, f := range n.Fields { 47 | Walk(v, f.Name) 48 | Walk(v, f.Value) 49 | } 50 | case *AccessExpr: 51 | Walk(v, n.Value) 52 | Walk(v, n.Name) 53 | case *IndexExpr: 54 | Walk(v, n.Value) 55 | Walk(v, n.Index) 56 | case *CallExpr: 57 | Walk(v, n.Value) 58 | for _, a := range n.Args { 59 | Walk(v, a) 60 | } 61 | case *UnaryExpr: 62 | Walk(v, n.Value) 63 | case *BinaryExpr: 64 | Walk(v, n.Left) 65 | Walk(v, n.Right) 66 | case *ParenExpr: 67 | Walk(v, n.Inner) 68 | default: 69 | panic(fmt.Sprintf("river/ast: unexpected node type %T", n)) 70 | } 71 | 72 | v.Visit(nil) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/riverfmt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/grafana/river/diag" 12 | "github.com/grafana/river/parser" 13 | "github.com/grafana/river/printer" 14 | ) 15 | 16 | func main() { 17 | err := run() 18 | 19 | var diags diag.Diagnostics 20 | if errors.As(err, &diags) { 21 | for _, diag := range diags { 22 | fmt.Fprintln(os.Stderr, diag) 23 | } 24 | os.Exit(1) 25 | } else if err != nil { 26 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | func run() error { 32 | var ( 33 | write bool 34 | ) 35 | 36 | fs := flag.NewFlagSet("riverfmt", flag.ExitOnError) 37 | fs.BoolVar(&write, "w", write, "write result to (source) file instead of stdout") 38 | 39 | if err := fs.Parse(os.Args[1:]); err != nil { 40 | return err 41 | } 42 | 43 | args := fs.Args() 44 | switch len(args) { 45 | case 0: 46 | if write { 47 | return fmt.Errorf("cannot use -w with standard input") 48 | } 49 | return format("", nil, os.Stdin, write) 50 | 51 | case 1: 52 | fi, err := os.Stat(args[0]) 53 | if err != nil { 54 | return err 55 | } 56 | if fi.IsDir() { 57 | return fmt.Errorf("cannot format a directory") 58 | } 59 | f, err := os.Open(args[0]) 60 | if err != nil { 61 | return err 62 | } 63 | defer f.Close() 64 | return format(args[0], fi, f, write) 65 | 66 | default: 67 | return fmt.Errorf("can only format one file") 68 | } 69 | } 70 | 71 | func format(filename string, fi os.FileInfo, r io.Reader, write bool) error { 72 | bb, err := io.ReadAll(r) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | f, err := parser.ParseFile(filename, bb) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | var buf bytes.Buffer 83 | if err := printer.Fprint(&buf, f); err != nil { 84 | return err 85 | } 86 | 87 | // Add a newline at the end 88 | _, _ = buf.Write([]byte{'\n'}) 89 | 90 | if !write { 91 | _, err := io.Copy(os.Stdout, &buf) 92 | return err 93 | } 94 | 95 | wf, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, fi.Mode().Perm()) 96 | if err != nil { 97 | return err 98 | } 99 | defer wf.Close() 100 | 101 | _, err = io.Copy(wf, &buf) 102 | return err 103 | } 104 | -------------------------------------------------------------------------------- /diag/diag.go: -------------------------------------------------------------------------------- 1 | // Package diag exposes error types used throughout River and a method to 2 | // pretty-print them to the screen. 3 | package diag 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/grafana/river/token" 9 | ) 10 | 11 | // Severity denotes the severity level of a diagnostic. The zero value of 12 | // severity is invalid. 13 | type Severity int 14 | 15 | // Supported severity levels. 16 | const ( 17 | SeverityLevelWarn Severity = iota + 1 18 | SeverityLevelError 19 | ) 20 | 21 | // Diagnostic is an individual diagnostic message. Diagnostic messages can have 22 | // different levels of severities. 23 | type Diagnostic struct { 24 | // Severity holds the severity level of this Diagnostic. 25 | Severity Severity 26 | 27 | // StartPos refers to a position in a file where this Diagnostic starts. 28 | StartPos token.Position 29 | 30 | // EndPos refers to an optional position in a file where this Diagnostic 31 | // ends. If EndPos is the zero value, the Diagnostic should be treated as 32 | // only covering a single character (i.e., StartPos == EndPos). 33 | // 34 | // When defined, EndPos must have the same Filename value as the StartPos. 35 | EndPos token.Position 36 | 37 | Message string 38 | Value string 39 | } 40 | 41 | // As allows d to be interpreted as a list of Diagnostics. 42 | func (d Diagnostic) As(v interface{}) bool { 43 | switch v := v.(type) { 44 | case *Diagnostics: 45 | *v = Diagnostics{d} 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | 52 | // Error implements error. 53 | func (d Diagnostic) Error() string { 54 | return fmt.Sprintf("%s: %s", d.StartPos, d.Message) 55 | } 56 | 57 | // Diagnostics is a collection of diagnostic messages. 58 | type Diagnostics []Diagnostic 59 | 60 | // Add adds an individual Diagnostic to the diagnostics list. 61 | func (ds *Diagnostics) Add(d Diagnostic) { 62 | *ds = append(*ds, d) 63 | } 64 | 65 | // Error implements error. 66 | func (ds Diagnostics) Error() string { 67 | switch len(ds) { 68 | case 0: 69 | return "no errors" 70 | case 1: 71 | return ds[0].Error() 72 | default: 73 | return fmt.Sprintf("%s (and %d more diagnostics)", ds[0], len(ds)-1) 74 | } 75 | } 76 | 77 | // ErrorOrNil returns an error interface if the list diagnostics is non-empty, 78 | // nil otherwise. 79 | func (ds Diagnostics) ErrorOrNil() error { 80 | if len(ds) == 0 { 81 | return nil 82 | } 83 | return ds 84 | } 85 | 86 | // HasErrors reports whether the list of Diagnostics contain any error-level 87 | // diagnostic. 88 | func (ds Diagnostics) HasErrors() bool { 89 | for _, d := range ds { 90 | if d.Severity == SeverityLevelError { 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | -------------------------------------------------------------------------------- /diag/printer.go: -------------------------------------------------------------------------------- 1 | package diag 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | "github.com/grafana/river/token" 12 | ) 13 | 14 | const tabWidth = 4 15 | 16 | // PrinterConfig controls different settings for the Printer. 17 | type PrinterConfig struct { 18 | // When Color is true, the printer will output with color and special 19 | // formatting characters (such as underlines). 20 | // 21 | // This should be disabled when not printing to a terminal. 22 | Color bool 23 | 24 | // ContextLinesBefore and ContextLinesAfter controls how many context lines 25 | // before and after the range of the diagnostic are printed. 26 | ContextLinesBefore, ContextLinesAfter int 27 | } 28 | 29 | // A Printer pretty-prints Diagnostics. 30 | type Printer struct { 31 | cfg PrinterConfig 32 | } 33 | 34 | // NewPrinter creates a new diagnostics Printer with the provided config. 35 | func NewPrinter(cfg PrinterConfig) *Printer { 36 | return &Printer{cfg: cfg} 37 | } 38 | 39 | // Fprint creates a Printer with default settings and prints diagnostics to the 40 | // provided writer. files is used to look up file contents by name for printing 41 | // diagnostics context. files may be set to nil to avoid printing context. 42 | func Fprint(w io.Writer, files map[string][]byte, diags Diagnostics) error { 43 | p := NewPrinter(PrinterConfig{ 44 | Color: false, 45 | ContextLinesBefore: 1, 46 | ContextLinesAfter: 1, 47 | }) 48 | return p.Fprint(w, files, diags) 49 | } 50 | 51 | // Fprint pretty-prints errors to a writer. files is used to look up file 52 | // contents by name when printing context. files may be nil to avoid printing 53 | // context. 54 | func (p *Printer) Fprint(w io.Writer, files map[string][]byte, diags Diagnostics) error { 55 | // Create a buffered writer since we'll have many small calls to Write while 56 | // we print errors. 57 | // 58 | // Buffers writers track the first write error received and will return it 59 | // (if any) when flushing, so we can ignore write errors throughout the code 60 | // until the very end. 61 | bw := bufio.NewWriter(w) 62 | 63 | for i, diag := range diags { 64 | p.printDiagnosticHeader(bw, diag) 65 | 66 | // If there's no ending position, set the ending position to be the same as 67 | // the start. 68 | if !diag.EndPos.Valid() { 69 | diag.EndPos = diag.StartPos 70 | } 71 | 72 | // We can print the file context if it was found. 73 | fileContents, foundFile := files[diag.StartPos.Filename] 74 | if foundFile && diag.StartPos.Filename == diag.EndPos.Filename { 75 | p.printRange(bw, fileContents, diag) 76 | } 77 | 78 | // Print a blank line to separate diagnostics. 79 | if i+1 < len(diags) { 80 | fmt.Fprintf(bw, "\n") 81 | } 82 | } 83 | 84 | return bw.Flush() 85 | } 86 | 87 | func (p *Printer) printDiagnosticHeader(w io.Writer, diag Diagnostic) { 88 | if p.cfg.Color { 89 | switch diag.Severity { 90 | case SeverityLevelError: 91 | cw := color.New(color.FgRed, color.Bold) 92 | _, _ = cw.Fprintf(w, "Error: ") 93 | case SeverityLevelWarn: 94 | cw := color.New(color.FgYellow, color.Bold) 95 | _, _ = cw.Fprintf(w, "Warning: ") 96 | } 97 | 98 | cw := color.New(color.Bold) 99 | _, _ = cw.Fprintf(w, "%s: %s\n", diag.StartPos, diag.Message) 100 | return 101 | } 102 | 103 | switch diag.Severity { 104 | case SeverityLevelError: 105 | _, _ = fmt.Fprintf(w, "Error: ") 106 | case SeverityLevelWarn: 107 | _, _ = fmt.Fprintf(w, "Warning: ") 108 | } 109 | fmt.Fprintf(w, "%s: %s\n", diag.StartPos, diag.Message) 110 | } 111 | 112 | func (p *Printer) printRange(w io.Writer, file []byte, diag Diagnostic) { 113 | var ( 114 | start = diag.StartPos 115 | end = diag.EndPos 116 | ) 117 | 118 | fmt.Fprintf(w, "\n") 119 | 120 | var ( 121 | lines = strings.Split(string(file), "\n") 122 | 123 | startLine = max(start.Line-p.cfg.ContextLinesBefore, 1) 124 | endLine = min(end.Line+p.cfg.ContextLinesAfter, len(lines)) 125 | 126 | multiline = end.Line-start.Line > 0 127 | ) 128 | 129 | prefixWidth := len(strconv.Itoa(endLine)) 130 | 131 | for lineNum := startLine; lineNum <= endLine; lineNum++ { 132 | line := lines[lineNum-1] 133 | 134 | // Print line number and margin. 135 | printPaddedNumber(w, prefixWidth, lineNum) 136 | fmt.Fprintf(w, " | ") 137 | 138 | if multiline { 139 | // Use 0 for the column number so we never consider the starting line for 140 | // showing |. 141 | if inRange(lineNum, 0, start, end) { 142 | fmt.Fprint(w, "| ") 143 | } else { 144 | fmt.Fprint(w, " ") 145 | } 146 | } 147 | 148 | // Print the line, but filter out any \r and replace tabs with spaces. 149 | for _, ch := range line { 150 | if ch == '\r' { 151 | continue 152 | } 153 | if ch == '\t' || ch == '\v' { 154 | printCh(w, tabWidth, ' ') 155 | continue 156 | } 157 | fmt.Fprintf(w, "%c", ch) 158 | } 159 | 160 | fmt.Fprintf(w, "\n") 161 | 162 | // Print the focus indicator if we're on a line that needs it. 163 | // 164 | // The focus indicator line must preserve whitespace present in the line 165 | // above it prior to the focus '^' characters. Tab characters are replaced 166 | // with spaces for consistent printing. 167 | if lineNum == start.Line || (multiline && lineNum == end.Line) { 168 | printCh(w, prefixWidth, ' ') // Add empty space where line number would be 169 | 170 | // Print the margin after the blank line number. On multi-line errors, 171 | // the arrow is printed all the way to the margin, with straight 172 | // lines going down in between the lines. 173 | switch { 174 | case multiline && lineNum == start.Line: 175 | // |_ would look like an incorrect right angle, so the second bar 176 | // is dropped. 177 | fmt.Fprintf(w, " | _") 178 | case multiline && lineNum == end.Line: 179 | fmt.Fprintf(w, " | |_") 180 | default: 181 | fmt.Fprintf(w, " | ") 182 | } 183 | 184 | p.printFocus(w, line, lineNum, diag) 185 | fmt.Fprintf(w, "\n") 186 | } 187 | } 188 | } 189 | 190 | // printFocus prints the focus indicator for the line number specified by line. 191 | // The contents of the line should be represented by data so whitespace can be 192 | // retained (injecting spaces where a tab should be, etc.). 193 | func (p *Printer) printFocus(w io.Writer, data string, line int, diag Diagnostic) { 194 | for i, ch := range data { 195 | column := i + 1 196 | 197 | if line == diag.EndPos.Line && column > diag.EndPos.Column { 198 | // Stop printing the formatting line after printing all the ^. 199 | break 200 | } 201 | 202 | blank := byte(' ') 203 | if diag.EndPos.Line-diag.StartPos.Line > 0 { 204 | blank = byte('_') 205 | } 206 | 207 | switch { 208 | case ch == '\t' || ch == '\v': 209 | printCh(w, tabWidth, blank) 210 | case inRange(line, column, diag.StartPos, diag.EndPos): 211 | fmt.Fprintf(w, "%c", '^') 212 | default: 213 | // Print a space. 214 | fmt.Fprintf(w, "%c", blank) 215 | } 216 | } 217 | } 218 | 219 | func inRange(line, col int, start, end token.Position) bool { 220 | if line < start.Line || line > end.Line { 221 | return false 222 | } 223 | 224 | switch line { 225 | case start.Line: 226 | // If the current line is on the starting line, we have to be past the 227 | // starting column. 228 | return col >= start.Column 229 | case end.Line: 230 | // If the current line is on the ending line, we have to be before the 231 | // final column. 232 | return col <= end.Column 233 | default: 234 | // Otherwise, every column across all the lines in between 235 | // is in the range. 236 | return true 237 | } 238 | } 239 | 240 | func printPaddedNumber(w io.Writer, width int, num int) { 241 | numStr := strconv.Itoa(num) 242 | for i := 0; i < width-len(numStr); i++ { 243 | _, _ = w.Write([]byte{' '}) 244 | } 245 | _, _ = w.Write([]byte(numStr)) 246 | } 247 | 248 | func printCh(w io.Writer, count int, ch byte) { 249 | for i := 0; i < count; i++ { 250 | _, _ = w.Write([]byte{ch}) 251 | } 252 | } 253 | 254 | func min(a, b int) int { 255 | if a < b { 256 | return a 257 | } 258 | return b 259 | } 260 | 261 | func max(a, b int) int { 262 | if a > b { 263 | return a 264 | } 265 | return b 266 | } 267 | -------------------------------------------------------------------------------- /diag/printer_test.go: -------------------------------------------------------------------------------- 1 | package diag_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/grafana/river/diag" 9 | "github.com/grafana/river/token" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestFprint(t *testing.T) { 14 | // In all tests below, the filename is "testfile" and the severity is an 15 | // error. 16 | 17 | tt := []struct { 18 | name string 19 | input string 20 | start, end token.Position 21 | diag diag.Diagnostic 22 | expect string 23 | }{ 24 | { 25 | name: "highlight on same line", 26 | start: token.Position{Line: 2, Column: 2}, 27 | end: token.Position{Line: 2, Column: 5}, 28 | input: `test.block "label" { 29 | attr = 1 30 | other_attr = 2 31 | }`, 32 | expect: `Error: testfile:2:2: synthetic error 33 | 34 | 1 | test.block "label" { 35 | 2 | attr = 1 36 | | ^^^^ 37 | 3 | other_attr = 2 38 | `, 39 | }, 40 | 41 | { 42 | name: "end positions should be optional", 43 | start: token.Position{Line: 1, Column: 4}, 44 | input: `foo,bar`, 45 | expect: `Error: testfile:1:4: synthetic error 46 | 47 | 1 | foo,bar 48 | | ^ 49 | `, 50 | }, 51 | 52 | { 53 | name: "padding should be inserted to fit line numbers of different lengths", 54 | start: token.Position{Line: 9, Column: 1}, 55 | end: token.Position{Line: 9, Column: 6}, 56 | input: `LINE_1 57 | LINE_2 58 | LINE_3 59 | LINE_4 60 | LINE_5 61 | LINE_6 62 | LINE_7 63 | LINE_8 64 | LINE_9 65 | LINE_10 66 | LINE_11`, 67 | expect: `Error: testfile:9:1: synthetic error 68 | 69 | 8 | LINE_8 70 | 9 | LINE_9 71 | | ^^^^^^ 72 | 10 | LINE_10 73 | `, 74 | }, 75 | 76 | { 77 | name: "errors which cross multiple lines can be printed from start of line", 78 | start: token.Position{Line: 2, Column: 1}, 79 | end: token.Position{Line: 6, Column: 7}, 80 | input: `FILE_BEGIN 81 | START 82 | TEXT 83 | TEXT 84 | TEXT 85 | DONE after 86 | FILE_END`, 87 | expect: `Error: testfile:2:1: synthetic error 88 | 89 | 1 | FILE_BEGIN 90 | 2 | START 91 | | _^^^^^ 92 | 3 | | TEXT 93 | 4 | | TEXT 94 | 5 | | TEXT 95 | 6 | | DONE after 96 | | |_____________^^^^ 97 | 7 | FILE_END 98 | `, 99 | }, 100 | 101 | { 102 | name: "errors which cross multiple lines can be printed from middle of line", 103 | start: token.Position{Line: 2, Column: 8}, 104 | end: token.Position{Line: 6, Column: 7}, 105 | input: `FILE_BEGIN 106 | before START 107 | TEXT 108 | TEXT 109 | TEXT 110 | DONE after 111 | FILE_END`, 112 | expect: `Error: testfile:2:8: synthetic error 113 | 114 | 1 | FILE_BEGIN 115 | 2 | before START 116 | | ________^^^^^ 117 | 3 | | TEXT 118 | 4 | | TEXT 119 | 5 | | TEXT 120 | 6 | | DONE after 121 | | |_____________^^^^ 122 | 7 | FILE_END 123 | `, 124 | }, 125 | } 126 | 127 | for _, tc := range tt { 128 | t.Run(tc.name, func(t *testing.T) { 129 | files := map[string][]byte{ 130 | "testfile": []byte(tc.input), 131 | } 132 | 133 | tc.start.Filename = "testfile" 134 | tc.end.Filename = "testfile" 135 | 136 | diags := diag.Diagnostics{{ 137 | Severity: diag.SeverityLevelError, 138 | StartPos: tc.start, 139 | EndPos: tc.end, 140 | Message: "synthetic error", 141 | }} 142 | 143 | var buf bytes.Buffer 144 | _ = diag.Fprint(&buf, files, diags) 145 | requireEqualStrings(t, tc.expect, buf.String()) 146 | }) 147 | } 148 | } 149 | 150 | func TestFprint_MultipleDiagnostics(t *testing.T) { 151 | fileA := `old_field = 15 152 | 3 & 4` 153 | fileB := `old_field = 22` 154 | 155 | files := map[string][]byte{ 156 | "file_a": []byte(fileA), 157 | "file_b": []byte(fileB), 158 | } 159 | 160 | diags := diag.Diagnostics{ 161 | { 162 | Severity: diag.SeverityLevelWarn, 163 | StartPos: token.Position{Filename: "file_a", Line: 1, Column: 1}, 164 | EndPos: token.Position{Filename: "file_a", Line: 1, Column: 9}, 165 | Message: "old_field is deprecated", 166 | }, 167 | { 168 | Severity: diag.SeverityLevelError, 169 | StartPos: token.Position{Filename: "file_a", Line: 2, Column: 3}, 170 | Message: "unrecognized operator &", 171 | }, 172 | { 173 | Severity: diag.SeverityLevelWarn, 174 | StartPos: token.Position{Filename: "file_b", Line: 1, Column: 1}, 175 | EndPos: token.Position{Filename: "file_b", Line: 1, Column: 9}, 176 | Message: "old_field is deprecated", 177 | }, 178 | } 179 | 180 | expect := `Warning: file_a:1:1: old_field is deprecated 181 | 182 | 1 | old_field = 15 183 | | ^^^^^^^^^ 184 | 2 | 3 & 4 185 | 186 | Error: file_a:2:3: unrecognized operator & 187 | 188 | 1 | old_field = 15 189 | 2 | 3 & 4 190 | | ^ 191 | 192 | Warning: file_b:1:1: old_field is deprecated 193 | 194 | 1 | old_field = 22 195 | | ^^^^^^^^^ 196 | ` 197 | 198 | var buf bytes.Buffer 199 | _ = diag.Fprint(&buf, files, diags) 200 | requireEqualStrings(t, expect, buf.String()) 201 | } 202 | 203 | // requireEqualStrings is like require.Equal with two strings but it 204 | // pretty-prints multiline strings to make it easier to compare. 205 | func requireEqualStrings(t *testing.T, expected, actual string) { 206 | if expected == actual { 207 | return 208 | } 209 | 210 | msg := fmt.Sprintf( 211 | "Not equal:\n"+ 212 | "raw expected: %#v\n"+ 213 | "raw actual : %#v\n"+ 214 | "\n"+ 215 | "expected:\n%s\n"+ 216 | "actual:\n%s\n", 217 | expected, actual, 218 | expected, actual, 219 | ) 220 | 221 | require.Fail(t, msg) 222 | } 223 | -------------------------------------------------------------------------------- /encoding/riverjson/types.go: -------------------------------------------------------------------------------- 1 | package riverjson 2 | 3 | // Various concrete types used to marshal River values. 4 | type ( 5 | // jsonStatement is a statement within a River body. 6 | jsonStatement interface{ isStatement() } 7 | 8 | // A jsonBody is a collection of statements. 9 | jsonBody = []jsonStatement 10 | 11 | // jsonBlock represents a River block as JSON. jsonBlock is a jsonStatement. 12 | jsonBlock struct { 13 | Name string `json:"name"` 14 | Type string `json:"type"` // Always "block" 15 | Label string `json:"label,omitempty"` 16 | Body []jsonStatement `json:"body"` 17 | } 18 | 19 | // jsonAttr represents a River attribute as JSON. jsonAttr is a 20 | // jsonStatement. 21 | jsonAttr struct { 22 | Name string `json:"name"` 23 | Type string `json:"type"` // Always "attr" 24 | Value jsonValue `json:"value"` 25 | } 26 | 27 | // jsonValue represents a single River value as JSON. 28 | jsonValue struct { 29 | Type string `json:"type"` 30 | Value interface{} `json:"value"` 31 | } 32 | 33 | // jsonObjectField represents a field within a River object. 34 | jsonObjectField struct { 35 | Key string `json:"key"` 36 | Value interface{} `json:"value"` 37 | } 38 | ) 39 | 40 | func (jsonBlock) isStatement() {} 41 | func (jsonAttr) isStatement() {} 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/river 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/ohler55/ojg v1.20.1 8 | github.com/stretchr/testify v1.8.4 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/mattn/go-colorable v0.1.13 // indirect 14 | github.com/mattn/go-isatty v0.0.17 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 4 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 5 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 6 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 7 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 8 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 9 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 10 | github.com/ohler55/ojg v1.20.1 h1:Io65sHjMjYPI7yuhUr8VdNmIQdYU6asKeFhOs8xgBnY= 11 | github.com/ohler55/ojg v1.20.1/go.mod h1:uHcD1ErbErC27Zhb5Df2jUjbseLLcmOCo6oxSr3jZxo= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 15 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 16 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 18 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /internal/reflectutil/walk.go: -------------------------------------------------------------------------------- 1 | package reflectutil 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/grafana/river/internal/rivertags" 7 | ) 8 | 9 | // GetOrAlloc returns the nested field of value corresponding to index. 10 | // GetOrAlloc panics if not given a struct. 11 | func GetOrAlloc(value reflect.Value, field rivertags.Field) reflect.Value { 12 | return GetOrAllocIndex(value, field.Index) 13 | } 14 | 15 | // GetOrAllocIndex returns the nested field of value corresponding to index. 16 | // GetOrAllocIndex panics if not given a struct. 17 | // 18 | // It is similar to [reflect/Value.FieldByIndex] but can handle traversing 19 | // through nil pointers. If allocate is true, GetOrAllocIndex allocates any 20 | // intermediate nil pointers while traversing the struct. 21 | func GetOrAllocIndex(value reflect.Value, index []int) reflect.Value { 22 | if len(index) == 1 { 23 | return value.Field(index[0]) 24 | } 25 | 26 | if value.Kind() != reflect.Struct { 27 | panic("GetOrAlloc must be given a Struct, but found " + value.Kind().String()) 28 | } 29 | 30 | for _, next := range index { 31 | value = deferencePointer(value).Field(next) 32 | } 33 | 34 | return value 35 | } 36 | 37 | func deferencePointer(value reflect.Value) reflect.Value { 38 | for value.Kind() == reflect.Pointer { 39 | if value.IsNil() { 40 | value.Set(reflect.New(value.Type().Elem())) 41 | } 42 | value = value.Elem() 43 | } 44 | 45 | return value 46 | } 47 | 48 | // Get returns the nested field of value corresponding to index. Get panics if 49 | // not given a struct. 50 | // 51 | // It is similar to [reflect/Value.FieldByIndex] but can handle traversing 52 | // through nil pointers. If Get traverses through a nil pointer, a non-settable 53 | // zero value for the final field is returned. 54 | func Get(value reflect.Value, field rivertags.Field) reflect.Value { 55 | if len(field.Index) == 1 { 56 | return value.Field(field.Index[0]) 57 | } 58 | 59 | if value.Kind() != reflect.Struct { 60 | panic("Get must be given a Struct, but found " + value.Kind().String()) 61 | } 62 | 63 | for i, next := range field.Index { 64 | for value.Kind() == reflect.Pointer { 65 | if value.IsNil() { 66 | return getZero(value, field.Index[i:]) 67 | } 68 | value = value.Elem() 69 | } 70 | 71 | value = value.Field(next) 72 | } 73 | 74 | return value 75 | } 76 | 77 | // getZero returns a non-settable zero value while walking value. 78 | func getZero(value reflect.Value, index []int) reflect.Value { 79 | typ := value.Type() 80 | 81 | for _, next := range index { 82 | for typ.Kind() == reflect.Pointer { 83 | typ = typ.Elem() 84 | } 85 | typ = typ.Field(next).Type 86 | } 87 | 88 | return reflect.Zero(typ) 89 | } 90 | -------------------------------------------------------------------------------- /internal/reflectutil/walk_test.go: -------------------------------------------------------------------------------- 1 | package reflectutil_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/grafana/river/internal/reflectutil" 8 | "github.com/grafana/river/internal/rivertags" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDeeplyNested_Access(t *testing.T) { 14 | type Struct struct { 15 | Field1 struct { 16 | Field2 struct { 17 | Field3 struct { 18 | Value string 19 | } 20 | } 21 | } 22 | } 23 | 24 | var s Struct 25 | s.Field1.Field2.Field3.Value = "Hello, world!" 26 | 27 | rv := reflect.ValueOf(&s).Elem() 28 | innerValue := reflectutil.GetOrAlloc(rv, rivertags.Field{Index: []int{0, 0, 0, 0}}) 29 | assert.True(t, innerValue.CanSet()) 30 | assert.Equal(t, reflect.String, innerValue.Kind()) 31 | } 32 | 33 | func TestDeeplyNested_Allocate(t *testing.T) { 34 | type Struct struct { 35 | Field1 *struct { 36 | Field2 *struct { 37 | Field3 *struct { 38 | Value string 39 | } 40 | } 41 | } 42 | } 43 | 44 | var s Struct 45 | 46 | rv := reflect.ValueOf(&s).Elem() 47 | innerValue := reflectutil.GetOrAlloc(rv, rivertags.Field{Index: []int{0, 0, 0, 0}}) 48 | require.True(t, innerValue.CanSet()) 49 | require.Equal(t, reflect.String, innerValue.Kind()) 50 | 51 | innerValue.Set(reflect.ValueOf("Hello, world!")) 52 | require.Equal(t, "Hello, world!", s.Field1.Field2.Field3.Value) 53 | } 54 | 55 | func TestDeeplyNested_NoAllocate(t *testing.T) { 56 | type Struct struct { 57 | Field1 *struct { 58 | Field2 *struct { 59 | Field3 *struct { 60 | Value string 61 | } 62 | } 63 | } 64 | } 65 | 66 | var s Struct 67 | 68 | rv := reflect.ValueOf(&s).Elem() 69 | innerValue := reflectutil.Get(rv, rivertags.Field{Index: []int{0, 0, 0, 0}}) 70 | assert.False(t, innerValue.CanSet()) 71 | assert.Equal(t, reflect.String, innerValue.Kind()) 72 | } 73 | -------------------------------------------------------------------------------- /internal/rivertags/rivertags_test.go: -------------------------------------------------------------------------------- 1 | package rivertags_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/grafana/river/internal/rivertags" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_Get(t *testing.T) { 13 | type Struct struct { 14 | IgnoreMe bool 15 | 16 | ReqAttr string `river:"req_attr,attr"` 17 | OptAttr string `river:"opt_attr,attr,optional"` 18 | ReqBlock struct{} `river:"req_block,block"` 19 | OptBlock struct{} `river:"opt_block,block,optional"` 20 | ReqEnum []struct{} `river:"req_enum,enum"` 21 | OptEnum []struct{} `river:"opt_enum,enum,optional"` 22 | Label string `river:",label"` 23 | } 24 | 25 | fs := rivertags.Get(reflect.TypeOf(Struct{})) 26 | 27 | expect := []rivertags.Field{ 28 | {[]string{"req_attr"}, []int{1}, rivertags.FlagAttr}, 29 | {[]string{"opt_attr"}, []int{2}, rivertags.FlagAttr | rivertags.FlagOptional}, 30 | {[]string{"req_block"}, []int{3}, rivertags.FlagBlock}, 31 | {[]string{"opt_block"}, []int{4}, rivertags.FlagBlock | rivertags.FlagOptional}, 32 | {[]string{"req_enum"}, []int{5}, rivertags.FlagEnum}, 33 | {[]string{"opt_enum"}, []int{6}, rivertags.FlagEnum | rivertags.FlagOptional}, 34 | {[]string{""}, []int{7}, rivertags.FlagLabel}, 35 | } 36 | 37 | require.Equal(t, expect, fs) 38 | } 39 | 40 | func TestEmbedded(t *testing.T) { 41 | type InnerStruct struct { 42 | InnerField1 string `river:"inner_field_1,attr"` 43 | InnerField2 string `river:"inner_field_2,attr"` 44 | } 45 | 46 | type Struct struct { 47 | Field1 string `river:"parent_field_1,attr"` 48 | InnerStruct 49 | Field2 string `river:"parent_field_2,attr"` 50 | } 51 | require.PanicsWithValue(t, "river: anonymous fields not supported rivertags_test.Struct.InnerStruct", func() { rivertags.Get(reflect.TypeOf(Struct{})) }) 52 | } 53 | 54 | func TestSquash(t *testing.T) { 55 | type InnerStruct struct { 56 | InnerField1 string `river:"inner_field_1,attr"` 57 | InnerField2 string `river:"inner_field_2,attr"` 58 | } 59 | 60 | type Struct struct { 61 | Field1 string `river:"parent_field_1,attr"` 62 | Inner InnerStruct `river:",squash"` 63 | Field2 string `river:"parent_field_2,attr"` 64 | } 65 | 66 | type StructWithPointer struct { 67 | Field1 string `river:"parent_field_1,attr"` 68 | Inner *InnerStruct `river:",squash"` 69 | Field2 string `river:"parent_field_2,attr"` 70 | } 71 | 72 | expect := []rivertags.Field{ 73 | { 74 | Name: []string{"parent_field_1"}, 75 | Index: []int{0}, 76 | Flags: rivertags.FlagAttr, 77 | }, 78 | { 79 | Name: []string{"inner_field_1"}, 80 | Index: []int{1, 0}, 81 | Flags: rivertags.FlagAttr, 82 | }, 83 | { 84 | Name: []string{"inner_field_2"}, 85 | Index: []int{1, 1}, 86 | Flags: rivertags.FlagAttr, 87 | }, 88 | { 89 | Name: []string{"parent_field_2"}, 90 | Index: []int{2}, 91 | Flags: rivertags.FlagAttr, 92 | }, 93 | } 94 | 95 | structActual := rivertags.Get(reflect.TypeOf(Struct{})) 96 | assert.Equal(t, expect, structActual) 97 | 98 | structPointerActual := rivertags.Get(reflect.TypeOf(StructWithPointer{})) 99 | assert.Equal(t, expect, structPointerActual) 100 | } 101 | 102 | func TestDeepSquash(t *testing.T) { 103 | type Inner2Struct struct { 104 | InnerField1 string `river:"inner_field_1,attr"` 105 | InnerField2 string `river:"inner_field_2,attr"` 106 | } 107 | 108 | type InnerStruct struct { 109 | Inner2Struct Inner2Struct `river:",squash"` 110 | } 111 | 112 | type Struct struct { 113 | Inner InnerStruct `river:",squash"` 114 | } 115 | 116 | expect := []rivertags.Field{ 117 | { 118 | Name: []string{"inner_field_1"}, 119 | Index: []int{0, 0, 0}, 120 | Flags: rivertags.FlagAttr, 121 | }, 122 | { 123 | Name: []string{"inner_field_2"}, 124 | Index: []int{0, 0, 1}, 125 | Flags: rivertags.FlagAttr, 126 | }, 127 | } 128 | 129 | structActual := rivertags.Get(reflect.TypeOf(Struct{})) 130 | assert.Equal(t, expect, structActual) 131 | } 132 | 133 | func Test_Get_Panics(t *testing.T) { 134 | expectPanic := func(t *testing.T, expect string, v interface{}) { 135 | t.Helper() 136 | require.PanicsWithValue(t, expect, func() { 137 | _ = rivertags.Get(reflect.TypeOf(v)) 138 | }) 139 | } 140 | 141 | t.Run("Tagged fields must be exported", func(t *testing.T) { 142 | type Struct struct { 143 | attr string `river:"field,attr"` // nolint:unused //nolint:rivertags 144 | } 145 | expect := `river: river tag found on unexported field at rivertags_test.Struct.attr` 146 | expectPanic(t, expect, Struct{}) 147 | }) 148 | 149 | t.Run("Options are required", func(t *testing.T) { 150 | type Struct struct { 151 | Attr string `river:"field"` //nolint:rivertags 152 | } 153 | expect := `river: field rivertags_test.Struct.Attr tag is missing options` 154 | expectPanic(t, expect, Struct{}) 155 | }) 156 | 157 | t.Run("Field names must be unique", func(t *testing.T) { 158 | type Struct struct { 159 | Attr string `river:"field1,attr"` 160 | Block string `river:"field1,block,optional"` //nolint:rivertags 161 | } 162 | expect := `river: field name field1 already used by rivertags_test.Struct.Attr` 163 | expectPanic(t, expect, Struct{}) 164 | }) 165 | 166 | t.Run("Name is required for non-label field", func(t *testing.T) { 167 | type Struct struct { 168 | Attr string `river:",attr"` //nolint:rivertags 169 | } 170 | expect := `river: non-empty field name required at rivertags_test.Struct.Attr` 171 | expectPanic(t, expect, Struct{}) 172 | }) 173 | 174 | t.Run("Only one label field may exist", func(t *testing.T) { 175 | type Struct struct { 176 | Label1 string `river:",label"` 177 | Label2 string `river:",label"` 178 | } 179 | expect := `river: label field already used by rivertags_test.Struct.Label2` 180 | expectPanic(t, expect, Struct{}) 181 | }) 182 | } 183 | -------------------------------------------------------------------------------- /internal/stdlib/constants.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | ) 7 | 8 | var constants = map[string]string{ 9 | "hostname": "", // Initialized via init function 10 | "os": runtime.GOOS, 11 | "arch": runtime.GOARCH, 12 | } 13 | 14 | func init() { 15 | hostname, err := os.Hostname() 16 | if err == nil { 17 | constants["hostname"] = hostname 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/stdlib/stdlib.go: -------------------------------------------------------------------------------- 1 | // Package stdlib contains standard library functions exposed to River configs. 2 | package stdlib 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/grafana/river/internal/value" 11 | "github.com/grafana/river/rivertypes" 12 | "github.com/ohler55/ojg/jp" 13 | "github.com/ohler55/ojg/oj" 14 | ) 15 | 16 | // Identifiers holds a list of stdlib identifiers by name. All interface{} 17 | // values are River-compatible values. 18 | // 19 | // Function identifiers are Go functions with exactly one non-error return 20 | // value, with an optionally supported error return value as the second return 21 | // value. 22 | var Identifiers = map[string]interface{}{ 23 | // See constants.go for the definition. 24 | "constants": constants, 25 | 26 | "env": os.Getenv, 27 | 28 | "nonsensitive": func(secret rivertypes.Secret) string { 29 | return string(secret) 30 | }, 31 | 32 | // concat is implemented as a raw function so it can bypass allocations 33 | // converting arguments into []interface{}. concat is optimized to allow it 34 | // to perform well when it is in the hot path for combining targets from many 35 | // other blocks. 36 | "concat": value.RawFunction(func(funcValue value.Value, args ...value.Value) (value.Value, error) { 37 | if len(args) == 0 { 38 | return value.Array(), nil 39 | } 40 | 41 | // finalSize is the final size of the resulting concatenated array. We type 42 | // check our arguments while computing what finalSize will be. 43 | var finalSize int 44 | for i, arg := range args { 45 | if arg.Type() != value.TypeArray { 46 | return value.Null, value.ArgError{ 47 | Function: funcValue, 48 | Argument: arg, 49 | Index: i, 50 | Inner: value.TypeError{ 51 | Value: arg, 52 | Expected: value.TypeArray, 53 | }, 54 | } 55 | } 56 | 57 | finalSize += arg.Len() 58 | } 59 | 60 | // Optimization: if there's only one array, we can just return it directly. 61 | // This is done *after* the previous loop to ensure that args[0] is a River 62 | // array. 63 | if len(args) == 1 { 64 | return args[0], nil 65 | } 66 | 67 | raw := make([]value.Value, 0, finalSize) 68 | for _, arg := range args { 69 | for i := 0; i < arg.Len(); i++ { 70 | raw = append(raw, arg.Index(i)) 71 | } 72 | } 73 | 74 | return value.Array(raw...), nil 75 | }), 76 | 77 | "json_decode": func(in string) (interface{}, error) { 78 | var res interface{} 79 | err := json.Unmarshal([]byte(in), &res) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return res, nil 84 | }, 85 | 86 | "json_path": func(jsonString string, path string) (interface{}, error) { 87 | jsonPathExpr, err := jp.ParseString(path) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | jsonExpr, err := oj.ParseString(jsonString) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return jsonPathExpr.Get(jsonExpr), nil 98 | }, 99 | 100 | "coalesce": value.RawFunction(func(funcValue value.Value, args ...value.Value) (value.Value, error) { 101 | if len(args) == 0 { 102 | return value.Null, nil 103 | } 104 | 105 | for _, arg := range args { 106 | if arg.Type() == value.TypeNull { 107 | continue 108 | } 109 | 110 | if !arg.Reflect().IsZero() { 111 | if argType := value.RiverType(arg.Reflect().Type()); (argType == value.TypeArray || argType == value.TypeObject) && arg.Len() == 0 { 112 | continue 113 | } 114 | 115 | return arg, nil 116 | } 117 | } 118 | 119 | return args[len(args)-1], nil 120 | }), 121 | 122 | "format": fmt.Sprintf, 123 | "join": strings.Join, 124 | "replace": strings.ReplaceAll, 125 | "split": strings.Split, 126 | "to_lower": strings.ToLower, 127 | "to_upper": strings.ToUpper, 128 | "trim": strings.Trim, 129 | "trim_prefix": strings.TrimPrefix, 130 | "trim_suffix": strings.TrimSuffix, 131 | "trim_space": strings.TrimSpace, 132 | } 133 | -------------------------------------------------------------------------------- /internal/value/capsule.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Capsule is a marker interface for Go values which forces a type to be 8 | // represented as a River capsule. This is useful for types whose underlying 9 | // value is not a capsule, such as: 10 | // 11 | // // Secret is a secret value. It would normally be a River string since the 12 | // // underlying Go type is string, but it's a capsule since it implements 13 | // // the Capsule interface. 14 | // type Secret string 15 | // 16 | // func (s Secret) RiverCapsule() {} 17 | // 18 | // Extension interfaces are used to describe additional behaviors for Capsules. 19 | // ConvertibleCapsule allows defining custom conversion rules to convert 20 | // between other Go values. 21 | type Capsule interface { 22 | RiverCapsule() 23 | } 24 | 25 | // ErrNoConversion is returned by implementations of ConvertibleCapsule to 26 | // denote that a custom conversion from or to a specific type is unavailable. 27 | var ErrNoConversion = fmt.Errorf("no custom capsule conversion available") 28 | 29 | // ConvertibleFromCapsule is a Capsule which supports custom conversion rules 30 | // from any Go type which is not the same as the capsule type. 31 | type ConvertibleFromCapsule interface { 32 | Capsule 33 | 34 | // ConvertFrom should modify the ConvertibleCapsule value based on the value 35 | // of src. 36 | // 37 | // ConvertFrom should return ErrNoConversion if no conversion is available 38 | // from src. 39 | ConvertFrom(src interface{}) error 40 | } 41 | 42 | // ConvertibleIntoCapsule is a Capsule which supports custom conversion rules 43 | // into any Go type which is not the same as the capsule type. 44 | type ConvertibleIntoCapsule interface { 45 | Capsule 46 | 47 | // ConvertInto should convert its value and store it into dst. dst will be a 48 | // pointer to a value which ConvertInto is expected to update. 49 | // 50 | // ConvertInto should return ErrNoConversion if no conversion into dst is 51 | // available. 52 | ConvertInto(dst interface{}) error 53 | } 54 | -------------------------------------------------------------------------------- /internal/value/decode_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/grafana/river/internal/value" 8 | ) 9 | 10 | func BenchmarkObjectDecode(b *testing.B) { 11 | b.StopTimer() 12 | 13 | // Create a value with 20 keys. 14 | source := make(map[string]string, 20) 15 | for i := 0; i < 20; i++ { 16 | var ( 17 | key = fmt.Sprintf("key_%d", i+1) 18 | value = fmt.Sprintf("value_%d", i+1) 19 | ) 20 | source[key] = value 21 | } 22 | 23 | sourceVal := value.Encode(source) 24 | 25 | b.StartTimer() 26 | for i := 0; i < b.N; i++ { 27 | var dst map[string]string 28 | _ = value.Decode(sourceVal, &dst) 29 | } 30 | } 31 | 32 | func BenchmarkObject(b *testing.B) { 33 | b.Run("Non-capsule", func(b *testing.B) { 34 | b.StopTimer() 35 | 36 | vals := make(map[string]value.Value) 37 | for i := 0; i < 20; i++ { 38 | vals[fmt.Sprintf("%d", i)] = value.Int(int64(i)) 39 | } 40 | 41 | b.StartTimer() 42 | for i := 0; i < b.N; i++ { 43 | _ = value.Object(vals) 44 | } 45 | }) 46 | 47 | b.Run("Capsule", func(b *testing.B) { 48 | b.StopTimer() 49 | 50 | vals := make(map[string]value.Value) 51 | for i := 0; i < 20; i++ { 52 | vals[fmt.Sprintf("%d", i)] = value.Encapsulate(make(chan int)) 53 | } 54 | 55 | b.StartTimer() 56 | for i := 0; i < b.N; i++ { 57 | _ = value.Object(vals) 58 | } 59 | }) 60 | } 61 | 62 | func BenchmarkArray(b *testing.B) { 63 | b.Run("Non-capsule", func(b *testing.B) { 64 | b.StopTimer() 65 | 66 | var vals []value.Value 67 | for i := 0; i < 20; i++ { 68 | vals = append(vals, value.Int(int64(i))) 69 | } 70 | 71 | b.StartTimer() 72 | for i := 0; i < b.N; i++ { 73 | _ = value.Array(vals...) 74 | } 75 | }) 76 | 77 | b.Run("Capsule", func(b *testing.B) { 78 | b.StopTimer() 79 | 80 | var vals []value.Value 81 | for i := 0; i < 20; i++ { 82 | vals = append(vals, value.Encapsulate(make(chan int))) 83 | } 84 | 85 | b.StartTimer() 86 | for i := 0; i < b.N; i++ { 87 | _ = value.Array(vals...) 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /internal/value/errors.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "fmt" 4 | 5 | // Error is used for reporting on a value-level error. It is the most general 6 | // type of error for a value. 7 | type Error struct { 8 | Value Value 9 | Inner error 10 | } 11 | 12 | // TypeError is used for reporting on a value having an unexpected type. 13 | type TypeError struct { 14 | // Value which caused the error. 15 | Value Value 16 | Expected Type 17 | } 18 | 19 | // Error returns the string form of the TypeError. 20 | func (te TypeError) Error() string { 21 | return fmt.Sprintf("expected %s, got %s", te.Expected, te.Value.Type()) 22 | } 23 | 24 | // Error returns the message of the decode error. 25 | func (de Error) Error() string { return de.Inner.Error() } 26 | 27 | // MissingKeyError is used for reporting that a value is missing a key. 28 | type MissingKeyError struct { 29 | Value Value 30 | Missing string 31 | } 32 | 33 | // Error returns the string form of the MissingKeyError. 34 | func (mke MissingKeyError) Error() string { 35 | return fmt.Sprintf("key %q does not exist", mke.Missing) 36 | } 37 | 38 | // ElementError is used to report on an error inside of an array. 39 | type ElementError struct { 40 | Value Value // The Array value 41 | Index int // The index of the element with the issue 42 | Inner error // The error from the element 43 | } 44 | 45 | // Error returns the text of the inner error. 46 | func (ee ElementError) Error() string { return ee.Inner.Error() } 47 | 48 | // FieldError is used to report on an invalid field inside an object. 49 | type FieldError struct { 50 | Value Value // The Object value 51 | Field string // The field name with the issue 52 | Inner error // The error from the field 53 | } 54 | 55 | // Error returns the text of the inner error. 56 | func (fe FieldError) Error() string { return fe.Inner.Error() } 57 | 58 | // ArgError is used to report on an invalid argument to a function. 59 | type ArgError struct { 60 | Function Value 61 | Argument Value 62 | Index int 63 | Inner error 64 | } 65 | 66 | // Error returns the text of the inner error. 67 | func (ae ArgError) Error() string { return ae.Inner.Error() } 68 | 69 | // WalkError walks err for all value-related errors in this package. 70 | // WalkError returns false if err is not an error from this package. 71 | func WalkError(err error, f func(err error)) bool { 72 | var foundOne bool 73 | 74 | nextError := err 75 | for nextError != nil { 76 | switch ne := nextError.(type) { 77 | case Error: 78 | f(nextError) 79 | nextError = ne.Inner 80 | foundOne = true 81 | case TypeError: 82 | f(nextError) 83 | nextError = nil 84 | foundOne = true 85 | case MissingKeyError: 86 | f(nextError) 87 | nextError = nil 88 | foundOne = true 89 | case ElementError: 90 | f(nextError) 91 | nextError = ne.Inner 92 | foundOne = true 93 | case FieldError: 94 | f(nextError) 95 | nextError = ne.Inner 96 | foundOne = true 97 | case ArgError: 98 | f(nextError) 99 | nextError = ne.Inner 100 | foundOne = true 101 | default: 102 | nextError = nil 103 | } 104 | } 105 | 106 | return foundOne 107 | } 108 | -------------------------------------------------------------------------------- /internal/value/number_value.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | nativeIntBits = reflect.TypeOf(int(0)).Bits() 11 | nativeUintBits = reflect.TypeOf(uint(0)).Bits() 12 | ) 13 | 14 | // NumberKind categorizes a type of Go number. 15 | type NumberKind uint8 16 | 17 | const ( 18 | // NumberKindInt represents an int-like type (e.g., int, int8, etc.). 19 | NumberKindInt NumberKind = iota 20 | // NumberKindUint represents a uint-like type (e.g., uint, uint8, etc.). 21 | NumberKindUint 22 | // NumberKindFloat represents both float32 and float64. 23 | NumberKindFloat 24 | ) 25 | 26 | // makeNumberKind converts a Go kind to a River kind. 27 | func makeNumberKind(k reflect.Kind) NumberKind { 28 | switch k { 29 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 30 | return NumberKindInt 31 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 32 | return NumberKindUint 33 | case reflect.Float32, reflect.Float64: 34 | return NumberKindFloat 35 | default: 36 | panic("river/value: makeNumberKind called with unsupported Kind value") 37 | } 38 | } 39 | 40 | // Number is a generic representation of Go numbers. It is intended to be 41 | // created on the fly for numerical operations when the real number type is not 42 | // known. 43 | type Number struct { 44 | // Value holds the raw data for the number. Note that for numberKindFloat, 45 | // value is the raw bits of the float64 and must be converted back to a 46 | // float64 before it can be used. 47 | value uint64 48 | 49 | bits uint8 // 8, 16, 32, 64, used for overflow checking 50 | k NumberKind // int, uint, float 51 | } 52 | 53 | func newNumberValue(v reflect.Value) Number { 54 | var ( 55 | val uint64 56 | bits int 57 | nk NumberKind 58 | ) 59 | 60 | switch v.Kind() { 61 | case reflect.Int: 62 | val, bits, nk = uint64(v.Int()), nativeIntBits, NumberKindInt 63 | case reflect.Int8: 64 | val, bits, nk = uint64(v.Int()), 8, NumberKindInt 65 | case reflect.Int16: 66 | val, bits, nk = uint64(v.Int()), 16, NumberKindInt 67 | case reflect.Int32: 68 | val, bits, nk = uint64(v.Int()), 32, NumberKindInt 69 | case reflect.Int64: 70 | val, bits, nk = uint64(v.Int()), 64, NumberKindInt 71 | case reflect.Uint: 72 | val, bits, nk = v.Uint(), nativeUintBits, NumberKindUint 73 | case reflect.Uint8: 74 | val, bits, nk = v.Uint(), 8, NumberKindUint 75 | case reflect.Uint16: 76 | val, bits, nk = v.Uint(), 16, NumberKindUint 77 | case reflect.Uint32: 78 | val, bits, nk = v.Uint(), 32, NumberKindUint 79 | case reflect.Uint64: 80 | val, bits, nk = v.Uint(), 64, NumberKindUint 81 | case reflect.Float32: 82 | val, bits, nk = math.Float64bits(v.Float()), 32, NumberKindFloat 83 | case reflect.Float64: 84 | val, bits, nk = math.Float64bits(v.Float()), 64, NumberKindFloat 85 | default: 86 | panic("river/value: unrecognized Go number type " + v.Kind().String()) 87 | } 88 | 89 | return Number{val, uint8(bits), nk} 90 | } 91 | 92 | // Kind returns the Number's NumberKind. 93 | func (nv Number) Kind() NumberKind { return nv.k } 94 | 95 | // Int converts the Number into an int64. 96 | func (nv Number) Int() int64 { 97 | if nv.k == NumberKindFloat { 98 | return int64(math.Float64frombits(nv.value)) 99 | } 100 | return int64(nv.value) 101 | } 102 | 103 | // Uint converts the Number into a uint64. 104 | func (nv Number) Uint() uint64 { 105 | if nv.k == NumberKindFloat { 106 | return uint64(math.Float64frombits(nv.value)) 107 | } 108 | return nv.value 109 | } 110 | 111 | // Float converts the Number into a float64. 112 | func (nv Number) Float() float64 { 113 | switch nv.k { 114 | case NumberKindInt: 115 | // Convert nv.value to an int64 before converting to a float64 so the sign 116 | // flag gets handled correctly. 117 | return float64(int64(nv.value)) 118 | case NumberKindFloat: 119 | return math.Float64frombits(nv.value) 120 | } 121 | return float64(nv.value) 122 | } 123 | 124 | // ToString converts the Number to a string. 125 | func (nv Number) ToString() string { 126 | switch nv.k { 127 | case NumberKindUint: 128 | return strconv.FormatUint(nv.value, 10) 129 | case NumberKindInt: 130 | return strconv.FormatInt(int64(nv.value), 10) 131 | case NumberKindFloat: 132 | return strconv.FormatFloat(math.Float64frombits(nv.value), 'f', -1, 64) 133 | } 134 | panic("river/value: unreachable") 135 | } 136 | -------------------------------------------------------------------------------- /internal/value/raw_function.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | // RawFunction allows creating function implementations using raw River values. 4 | // This is useful for functions which wish to operate over dynamic types while 5 | // avoiding decoding to interface{} for performance reasons. 6 | // 7 | // The func value itself is provided as an argument so error types can be 8 | // filled. 9 | type RawFunction func(funcValue Value, args ...Value) (Value, error) 10 | -------------------------------------------------------------------------------- /internal/value/tag_cache.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/grafana/river/internal/rivertags" 7 | ) 8 | 9 | // tagsCache caches the river tags for a struct type. This is never cleared, 10 | // but since most structs will be statically created throughout the lifetime 11 | // of the process, this will consume a negligible amount of memory. 12 | var tagsCache = make(map[reflect.Type]*objectFields) 13 | 14 | func getCachedTags(t reflect.Type) *objectFields { 15 | if t.Kind() != reflect.Struct { 16 | panic("getCachedTags called with non-struct type") 17 | } 18 | 19 | if entry, ok := tagsCache[t]; ok { 20 | return entry 21 | } 22 | 23 | ff := rivertags.Get(t) 24 | 25 | // Build a tree of keys. 26 | tree := &objectFields{ 27 | fields: make(map[string]rivertags.Field), 28 | nestedFields: make(map[string]*objectFields), 29 | keys: []string{}, 30 | } 31 | 32 | for _, f := range ff { 33 | if f.Flags&rivertags.FlagLabel != 0 { 34 | // Skip over label tags. 35 | tree.labelField = f 36 | continue 37 | } 38 | 39 | node := tree 40 | for i, name := range f.Name { 41 | // Add to the list of keys if this is a new key. 42 | if node.Has(name) == objectKeyTypeInvalid { 43 | node.keys = append(node.keys, name) 44 | } 45 | 46 | if i+1 == len(f.Name) { 47 | // Last fragment, add as a field. 48 | node.fields[name] = f 49 | continue 50 | } 51 | 52 | inner, ok := node.nestedFields[name] 53 | if !ok { 54 | inner = &objectFields{ 55 | fields: make(map[string]rivertags.Field), 56 | nestedFields: make(map[string]*objectFields), 57 | keys: []string{}, 58 | } 59 | node.nestedFields[name] = inner 60 | } 61 | node = inner 62 | } 63 | } 64 | 65 | tagsCache[t] = tree 66 | return tree 67 | } 68 | 69 | // objectFields is a parsed tree of fields in rivertags. It forms a tree where 70 | // leaves are nested fields (e.g., for block names that have multiple name 71 | // fragments) and nodes are the fields themselves. 72 | type objectFields struct { 73 | fields map[string]rivertags.Field 74 | nestedFields map[string]*objectFields 75 | keys []string // Combination of fields + nestedFields 76 | labelField rivertags.Field 77 | } 78 | 79 | type objectKeyType int 80 | 81 | const ( 82 | objectKeyTypeInvalid objectKeyType = iota 83 | objectKeyTypeField 84 | objectKeyTypeNestedField 85 | ) 86 | 87 | // Has returns whether name exists as a field or a nested key inside keys. 88 | // Returns objectKeyTypeInvalid if name does not exist as either. 89 | func (of *objectFields) Has(name string) objectKeyType { 90 | if _, ok := of.fields[name]; ok { 91 | return objectKeyTypeField 92 | } 93 | if _, ok := of.nestedFields[name]; ok { 94 | return objectKeyTypeNestedField 95 | } 96 | return objectKeyTypeInvalid 97 | } 98 | 99 | // Len returns the number of named keys. 100 | func (of *objectFields) Len() int { return len(of.keys) } 101 | 102 | // Keys returns all named keys (fields and nested fields). 103 | func (of *objectFields) Keys() []string { return of.keys } 104 | 105 | // Field gets a non-nested field. Returns false if name is a nested field. 106 | func (of *objectFields) Field(name string) (rivertags.Field, bool) { 107 | f, ok := of.fields[name] 108 | return f, ok 109 | } 110 | 111 | // NestedField gets a named nested field entry. Returns false if name is not a 112 | // nested field. 113 | func (of *objectFields) NestedField(name string) (*objectFields, bool) { 114 | nk, ok := of.nestedFields[name] 115 | return nk, ok 116 | } 117 | 118 | // LabelField returns the field used for the label (if any). 119 | func (of *objectFields) LabelField() (rivertags.Field, bool) { 120 | return of.labelField, of.labelField.Index != nil 121 | } 122 | -------------------------------------------------------------------------------- /internal/value/type.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Type represents the type of a River value loosely. For example, a Value may 9 | // be TypeArray, but this does not imply anything about the type of that 10 | // array's elements (all of which may be any type). 11 | // 12 | // TypeCapsule is a special type which encapsulates arbitrary Go values. 13 | type Type uint8 14 | 15 | // Supported Type values. 16 | const ( 17 | TypeNull Type = iota 18 | TypeNumber 19 | TypeString 20 | TypeBool 21 | TypeArray 22 | TypeObject 23 | TypeFunction 24 | TypeCapsule 25 | ) 26 | 27 | var typeStrings = [...]string{ 28 | TypeNull: "null", 29 | TypeNumber: "number", 30 | TypeString: "string", 31 | TypeBool: "bool", 32 | TypeArray: "array", 33 | TypeObject: "object", 34 | TypeFunction: "function", 35 | TypeCapsule: "capsule", 36 | } 37 | 38 | // String returns the name of t. 39 | func (t Type) String() string { 40 | if int(t) < len(typeStrings) { 41 | return typeStrings[t] 42 | } 43 | return fmt.Sprintf("Type(%d)", t) 44 | } 45 | 46 | // GoString returns the name of t. 47 | func (t Type) GoString() string { return t.String() } 48 | 49 | // RiverType returns the River type from the Go type. 50 | // 51 | // Go types map to River types using the following rules: 52 | // 53 | // 1. Go numbers (ints, uints, floats) map to a River number. 54 | // 2. Go strings map to a River string. 55 | // 3. Go bools map to a River bool. 56 | // 4. Go arrays and slices map to a River array. 57 | // 5. Go map[string]T map to a River object. 58 | // 6. Go structs map to a River object, provided they have at least one field 59 | // with a river tag. 60 | // 7. Valid Go functions map to a River function. 61 | // 8. Go interfaces map to a River capsule. 62 | // 9. All other Go values map to a River capsule. 63 | // 64 | // Go functions are only valid for River if they have one non-error return type 65 | // (the first return type) and one optional error return type (the second 66 | // return type). Other function types are treated as capsules. 67 | // 68 | // As an exception, any type which implements the Capsule interface is forced 69 | // to be a capsule. 70 | func RiverType(t reflect.Type) Type { 71 | // We don't know if the RiverCapsule interface is implemented for a pointer 72 | // or non-pointer type, so we have to check before and after dereferencing. 73 | 74 | for t.Kind() == reflect.Pointer { 75 | switch { 76 | case t.Implements(goCapsule): 77 | return TypeCapsule 78 | case t.Implements(goTextMarshaler): 79 | return TypeString 80 | } 81 | 82 | t = t.Elem() 83 | } 84 | 85 | switch { 86 | case t.Implements(goCapsule): 87 | return TypeCapsule 88 | case t.Implements(goTextMarshaler): 89 | return TypeString 90 | case t == goDuration: 91 | return TypeString 92 | } 93 | 94 | switch t.Kind() { 95 | case reflect.Invalid: 96 | return TypeNull 97 | 98 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 99 | return TypeNumber 100 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 101 | return TypeNumber 102 | case reflect.Float32, reflect.Float64: 103 | return TypeNumber 104 | 105 | case reflect.String: 106 | return TypeString 107 | 108 | case reflect.Bool: 109 | return TypeBool 110 | 111 | case reflect.Array, reflect.Slice: 112 | if inner := t.Elem(); inner.Kind() == reflect.Struct { 113 | if _, labeled := getCachedTags(inner).LabelField(); labeled { 114 | // An slice/array of labeled blocks is an object, where each label is a 115 | // top-level key. 116 | return TypeObject 117 | } 118 | } 119 | return TypeArray 120 | 121 | case reflect.Map: 122 | if t.Key() != goString { 123 | // Objects must be keyed by string. Anything else is forced to be a 124 | // Capsule. 125 | return TypeCapsule 126 | } 127 | return TypeObject 128 | 129 | case reflect.Struct: 130 | if getCachedTags(t).Len() == 0 { 131 | return TypeCapsule 132 | } 133 | return TypeObject 134 | 135 | case reflect.Func: 136 | switch t.NumOut() { 137 | case 1: 138 | if t.Out(0) == goError { 139 | return TypeCapsule 140 | } 141 | return TypeFunction 142 | case 2: 143 | if t.Out(0) == goError || t.Out(1) != goError { 144 | return TypeCapsule 145 | } 146 | return TypeFunction 147 | default: 148 | return TypeCapsule 149 | } 150 | 151 | case reflect.Interface: 152 | return TypeCapsule 153 | 154 | default: 155 | return TypeCapsule 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /internal/value/type_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/grafana/river/internal/value" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type customCapsule bool 12 | 13 | var _ value.Capsule = (customCapsule)(false) 14 | 15 | func (customCapsule) RiverCapsule() {} 16 | 17 | var typeTests = []struct { 18 | input interface{} 19 | expect value.Type 20 | }{ 21 | {int(0), value.TypeNumber}, 22 | {int8(0), value.TypeNumber}, 23 | {int16(0), value.TypeNumber}, 24 | {int32(0), value.TypeNumber}, 25 | {int64(0), value.TypeNumber}, 26 | {uint(0), value.TypeNumber}, 27 | {uint8(0), value.TypeNumber}, 28 | {uint16(0), value.TypeNumber}, 29 | {uint32(0), value.TypeNumber}, 30 | {uint64(0), value.TypeNumber}, 31 | {float32(0), value.TypeNumber}, 32 | {float64(0), value.TypeNumber}, 33 | 34 | {string(""), value.TypeString}, 35 | 36 | {bool(false), value.TypeBool}, 37 | 38 | {[...]int{0, 1, 2}, value.TypeArray}, 39 | {[]int{0, 1, 2}, value.TypeArray}, 40 | 41 | // Struct with no River tags is a capsule. 42 | {struct{}{}, value.TypeCapsule}, 43 | 44 | // A slice of labeled blocks should be an object. 45 | {[]struct { 46 | Label string `river:",label"` 47 | }{}, value.TypeObject}, 48 | 49 | {map[string]interface{}{}, value.TypeObject}, 50 | 51 | // Go functions must have one non-error return type and one optional error 52 | // return type to be River functions. Everything else is a capsule. 53 | {(func() int)(nil), value.TypeFunction}, 54 | {(func() (int, error))(nil), value.TypeFunction}, 55 | {(func())(nil), value.TypeCapsule}, // Must have non-error return type 56 | {(func() error)(nil), value.TypeCapsule}, // First return type must be non-error 57 | {(func() (error, int))(nil), value.TypeCapsule}, // First return type must be non-error 58 | {(func() (error, error))(nil), value.TypeCapsule}, // First return type must be non-error 59 | {(func() (int, int))(nil), value.TypeCapsule}, // Second return type must be error 60 | {(func() (int, int, int))(nil), value.TypeCapsule}, // Can only have 1 or 2 return types 61 | 62 | {make(chan struct{}), value.TypeCapsule}, 63 | {map[bool]interface{}{}, value.TypeCapsule}, // Maps with non-string types are capsules 64 | 65 | // Types with capsule markers should be capsules. 66 | {customCapsule(false), value.TypeCapsule}, 67 | {(*customCapsule)(nil), value.TypeCapsule}, 68 | {(**customCapsule)(nil), value.TypeCapsule}, 69 | } 70 | 71 | func Test_RiverType(t *testing.T) { 72 | for _, tc := range typeTests { 73 | rt := reflect.TypeOf(tc.input) 74 | 75 | t.Run(rt.String(), func(t *testing.T) { 76 | actual := value.RiverType(rt) 77 | require.Equal(t, tc.expect, actual, "Unexpected type for %#v", tc.input) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/value/value_object.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/grafana/river/internal/reflectutil" 7 | ) 8 | 9 | // structWrapper allows for partially traversing structs which contain fields 10 | // representing blocks. This is required due to how block names and labels 11 | // change the object representation. 12 | // 13 | // If a block name is a.b.c, then it is represented as three nested objects: 14 | // 15 | // { 16 | // a = { 17 | // b = { 18 | // c = { /* block contents */ }, 19 | // }, 20 | // } 21 | // } 22 | // 23 | // Similarly, if a block name is labeled (a.b.c "label"), then the label is the 24 | // top-level key after c. 25 | // 26 | // structWrapper exposes Len, Keys, and Key methods similar to Value to allow 27 | // traversing through the synthetic object. The values it returns are 28 | // structWrappers. 29 | // 30 | // Code in value.go MUST check to see if a struct is a structWrapper *before* 31 | // checking the value kind to ensure the appropriate methods are invoked. 32 | type structWrapper struct { 33 | structVal reflect.Value 34 | fields *objectFields 35 | label string // Non-empty string if this struct is wrapped in a label. 36 | } 37 | 38 | func wrapStruct(val reflect.Value, keepLabel bool) structWrapper { 39 | if val.Kind() != reflect.Struct { 40 | panic("river/value: wrapStruct called on non-struct value") 41 | } 42 | 43 | fields := getCachedTags(val.Type()) 44 | 45 | var label string 46 | if f, ok := fields.LabelField(); ok && keepLabel { 47 | label = reflectutil.Get(val, f).String() 48 | } 49 | 50 | return structWrapper{ 51 | structVal: val, 52 | fields: fields, 53 | label: label, 54 | } 55 | } 56 | 57 | // Value turns sw into a value. 58 | func (sw structWrapper) Value() Value { 59 | return Value{ 60 | rv: reflect.ValueOf(sw), 61 | ty: TypeObject, 62 | } 63 | } 64 | 65 | func (sw structWrapper) Len() int { 66 | if len(sw.label) > 0 { 67 | return 1 68 | } 69 | return sw.fields.Len() 70 | } 71 | 72 | func (sw structWrapper) Keys() []string { 73 | if len(sw.label) > 0 { 74 | return []string{sw.label} 75 | } 76 | return sw.fields.Keys() 77 | } 78 | 79 | func (sw structWrapper) Key(key string) (index Value, ok bool) { 80 | if len(sw.label) > 0 { 81 | if key != sw.label { 82 | return 83 | } 84 | next := reflect.ValueOf(structWrapper{ 85 | structVal: sw.structVal, 86 | fields: sw.fields, 87 | // Unset the label now that we've traversed it 88 | }) 89 | return Value{rv: next, ty: TypeObject}, true 90 | } 91 | 92 | keyType := sw.fields.Has(key) 93 | 94 | switch keyType { 95 | case objectKeyTypeInvalid: 96 | return // No such key 97 | 98 | case objectKeyTypeNestedField: 99 | // Continue traversing. 100 | nextNode, _ := sw.fields.NestedField(key) 101 | return Value{ 102 | rv: reflect.ValueOf(structWrapper{ 103 | structVal: sw.structVal, 104 | fields: nextNode, 105 | }), 106 | ty: TypeObject, 107 | }, true 108 | 109 | case objectKeyTypeField: 110 | f, _ := sw.fields.Field(key) 111 | val, err := sw.structVal.FieldByIndexErr(f.Index) 112 | if err != nil { 113 | return Null, true 114 | } 115 | return makeValue(val), true 116 | } 117 | 118 | panic("river/value: unreachable") 119 | } 120 | -------------------------------------------------------------------------------- /internal/value/value_object_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/river/internal/value" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // TestBlockRepresentation ensures that the struct tags for blocks are 11 | // represented correctly. 12 | func TestBlockRepresentation(t *testing.T) { 13 | type UnlabledBlock struct { 14 | Value int `river:"value,attr"` 15 | } 16 | type LabeledBlock struct { 17 | Value int `river:"value,attr"` 18 | Label string `river:",label"` 19 | } 20 | type OuterBlock struct { 21 | Attr1 string `river:"attr_1,attr"` 22 | Attr2 string `river:"attr_2,attr"` 23 | 24 | UnlabledBlock1 UnlabledBlock `river:"unlabeled.a,block"` 25 | UnlabledBlock2 UnlabledBlock `river:"unlabeled.b,block"` 26 | UnlabledBlock3 UnlabledBlock `river:"other_unlabeled,block"` 27 | 28 | LabeledBlock1 LabeledBlock `river:"labeled.a,block"` 29 | LabeledBlock2 LabeledBlock `river:"labeled.b,block"` 30 | LabeledBlock3 LabeledBlock `river:"other_labeled,block"` 31 | } 32 | 33 | val := OuterBlock{ 34 | Attr1: "value_1", 35 | Attr2: "value_2", 36 | UnlabledBlock1: UnlabledBlock{ 37 | Value: 1, 38 | }, 39 | UnlabledBlock2: UnlabledBlock{ 40 | Value: 2, 41 | }, 42 | UnlabledBlock3: UnlabledBlock{ 43 | Value: 3, 44 | }, 45 | LabeledBlock1: LabeledBlock{ 46 | Value: 4, 47 | Label: "label_a", 48 | }, 49 | LabeledBlock2: LabeledBlock{ 50 | Value: 5, 51 | Label: "label_b", 52 | }, 53 | LabeledBlock3: LabeledBlock{ 54 | Value: 6, 55 | Label: "label_c", 56 | }, 57 | } 58 | 59 | t.Run("Map decode", func(t *testing.T) { 60 | var m map[string]interface{} 61 | require.NoError(t, value.Decode(value.Encode(val), &m)) 62 | 63 | type object = map[string]interface{} 64 | 65 | expect := object{ 66 | "attr_1": "value_1", 67 | "attr_2": "value_2", 68 | "unlabeled": object{ 69 | "a": object{"value": 1}, 70 | "b": object{"value": 2}, 71 | }, 72 | "other_unlabeled": object{"value": 3}, 73 | "labeled": object{ 74 | "a": object{ 75 | "label_a": object{"value": 4}, 76 | }, 77 | "b": object{ 78 | "label_b": object{"value": 5}, 79 | }, 80 | }, 81 | "other_labeled": object{ 82 | "label_c": object{"value": 6}, 83 | }, 84 | } 85 | 86 | require.Equal(t, m, expect) 87 | }) 88 | 89 | t.Run("Object decode from other object", func(t *testing.T) { 90 | // Decode into a separate type which is structurally identical but not 91 | // literally the same. 92 | type OuterBlock2 OuterBlock 93 | 94 | var actualVal OuterBlock2 95 | require.NoError(t, value.Decode(value.Encode(val), &actualVal)) 96 | require.Equal(t, val, OuterBlock(actualVal)) 97 | }) 98 | } 99 | 100 | // TestSquashedBlockRepresentation ensures that the struct tags for squashed 101 | // blocks are represented correctly. 102 | func TestSquashedBlockRepresentation(t *testing.T) { 103 | type InnerStruct struct { 104 | InnerField1 string `river:"inner_field_1,attr,optional"` 105 | InnerField2 string `river:"inner_field_2,attr,optional"` 106 | } 107 | 108 | type OuterStruct struct { 109 | OuterField1 string `river:"outer_field_1,attr,optional"` 110 | Inner InnerStruct `river:",squash"` 111 | OuterField2 string `river:"outer_field_2,attr,optional"` 112 | } 113 | 114 | val := OuterStruct{ 115 | OuterField1: "value1", 116 | Inner: InnerStruct{ 117 | InnerField1: "value3", 118 | InnerField2: "value4", 119 | }, 120 | OuterField2: "value2", 121 | } 122 | 123 | t.Run("Map decode", func(t *testing.T) { 124 | var m map[string]interface{} 125 | require.NoError(t, value.Decode(value.Encode(val), &m)) 126 | 127 | type object = map[string]interface{} 128 | 129 | expect := object{ 130 | "outer_field_1": "value1", 131 | "inner_field_1": "value3", 132 | "inner_field_2": "value4", 133 | "outer_field_2": "value2", 134 | } 135 | 136 | require.Equal(t, m, expect) 137 | }) 138 | } 139 | 140 | func TestSliceOfBlocks(t *testing.T) { 141 | type UnlabledBlock struct { 142 | Value int `river:"value,attr"` 143 | } 144 | type LabeledBlock struct { 145 | Value int `river:"value,attr"` 146 | Label string `river:",label"` 147 | } 148 | type OuterBlock struct { 149 | Attr1 string `river:"attr_1,attr"` 150 | Attr2 string `river:"attr_2,attr"` 151 | 152 | Unlabeled []UnlabledBlock `river:"unlabeled,block"` 153 | Labeled []LabeledBlock `river:"labeled,block"` 154 | } 155 | 156 | val := OuterBlock{ 157 | Attr1: "value_1", 158 | Attr2: "value_2", 159 | Unlabeled: []UnlabledBlock{ 160 | {Value: 1}, 161 | {Value: 2}, 162 | {Value: 3}, 163 | }, 164 | Labeled: []LabeledBlock{ 165 | {Label: "label_a", Value: 4}, 166 | {Label: "label_b", Value: 5}, 167 | {Label: "label_c", Value: 6}, 168 | }, 169 | } 170 | 171 | t.Run("Map decode", func(t *testing.T) { 172 | var m map[string]interface{} 173 | require.NoError(t, value.Decode(value.Encode(val), &m)) 174 | 175 | type object = map[string]interface{} 176 | type list = []interface{} 177 | 178 | expect := object{ 179 | "attr_1": "value_1", 180 | "attr_2": "value_2", 181 | "unlabeled": list{ 182 | object{"value": 1}, 183 | object{"value": 2}, 184 | object{"value": 3}, 185 | }, 186 | "labeled": object{ 187 | "label_a": object{"value": 4}, 188 | "label_b": object{"value": 5}, 189 | "label_c": object{"value": 6}, 190 | }, 191 | } 192 | 193 | require.Equal(t, m, expect) 194 | }) 195 | 196 | t.Run("Object decode from other object", func(t *testing.T) { 197 | // Decode into a separate type which is structurally identical but not 198 | // literally the same. 199 | type OuterBlock2 OuterBlock 200 | 201 | var actualVal OuterBlock2 202 | require.NoError(t, value.Decode(value.Encode(val), &actualVal)) 203 | require.Equal(t, val, OuterBlock(actualVal)) 204 | }) 205 | } 206 | -------------------------------------------------------------------------------- /internal/value/value_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "testing" 7 | 8 | "github.com/grafana/river/internal/value" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // TestEncodeKeyLookup tests where Go values are retained correctly 13 | // throughout values with a key lookup. 14 | func TestEncodeKeyLookup(t *testing.T) { 15 | type Body struct { 16 | Data pointerMarshaler `river:"data,attr"` 17 | } 18 | 19 | tt := []struct { 20 | name string 21 | encodeTarget any 22 | key string 23 | 24 | expectBodyType value.Type 25 | expectKeyExists bool 26 | expectKeyValue value.Value 27 | expectKeyType value.Type 28 | }{ 29 | { 30 | name: "Struct Encode data Key", 31 | encodeTarget: &Body{}, 32 | key: "data", 33 | expectBodyType: value.TypeObject, 34 | expectKeyExists: true, 35 | expectKeyValue: value.String("Hello, world!"), 36 | expectKeyType: value.TypeString, 37 | }, 38 | { 39 | name: "Struct Encode Missing Key", 40 | encodeTarget: &Body{}, 41 | key: "missing", 42 | expectBodyType: value.TypeObject, 43 | expectKeyExists: false, 44 | expectKeyValue: value.Null, 45 | expectKeyType: value.TypeNull, 46 | }, 47 | { 48 | name: "Map Encode data Key", 49 | encodeTarget: map[string]string{"data": "Hello, world!"}, 50 | key: "data", 51 | expectBodyType: value.TypeObject, 52 | expectKeyExists: true, 53 | expectKeyValue: value.String("Hello, world!"), 54 | expectKeyType: value.TypeString, 55 | }, 56 | { 57 | name: "Map Encode Missing Key", 58 | encodeTarget: map[string]string{"data": "Hello, world!"}, 59 | key: "missing", 60 | expectBodyType: value.TypeObject, 61 | expectKeyExists: false, 62 | expectKeyValue: value.Null, 63 | expectKeyType: value.TypeNull, 64 | }, 65 | { 66 | name: "Map Encode empty value Key", 67 | encodeTarget: map[string]string{"data": ""}, 68 | key: "data", 69 | expectBodyType: value.TypeObject, 70 | expectKeyExists: true, 71 | expectKeyValue: value.String(""), 72 | expectKeyType: value.TypeString, 73 | }, 74 | } 75 | 76 | for _, tc := range tt { 77 | t.Run(tc.name, func(t *testing.T) { 78 | bodyVal := value.Encode(tc.encodeTarget) 79 | require.Equal(t, tc.expectBodyType, bodyVal.Type()) 80 | 81 | val, ok := bodyVal.Key(tc.key) 82 | require.Equal(t, tc.expectKeyExists, ok) 83 | require.Equal(t, tc.expectKeyType, val.Type()) 84 | switch val.Type() { 85 | case value.TypeString: 86 | require.Equal(t, tc.expectKeyValue.Text(), val.Text()) 87 | case value.TypeNull: 88 | require.Equal(t, tc.expectKeyValue, val) 89 | default: 90 | require.Fail(t, "unexpected value type (this switch can be expanded)") 91 | } 92 | }) 93 | } 94 | } 95 | 96 | // TestEncodeNoKeyLookup tests where Go values are retained correctly 97 | // throughout values without a key lookup. 98 | func TestEncodeNoKeyLookup(t *testing.T) { 99 | tt := []struct { 100 | name string 101 | encodeTarget any 102 | key string 103 | 104 | expectBodyType value.Type 105 | expectBodyText string 106 | }{ 107 | { 108 | name: "Encode", 109 | encodeTarget: &pointerMarshaler{}, 110 | expectBodyType: value.TypeString, 111 | expectBodyText: "Hello, world!", 112 | }, 113 | } 114 | 115 | for _, tc := range tt { 116 | t.Run(tc.name, func(t *testing.T) { 117 | bodyVal := value.Encode(tc.encodeTarget) 118 | require.Equal(t, tc.expectBodyType, bodyVal.Type()) 119 | require.Equal(t, "Hello, world!", bodyVal.Text()) 120 | }) 121 | } 122 | } 123 | 124 | type pointerMarshaler struct{} 125 | 126 | func (*pointerMarshaler) MarshalText() ([]byte, error) { 127 | return []byte("Hello, world!"), nil 128 | } 129 | 130 | func TestValue_Call(t *testing.T) { 131 | t.Run("simple", func(t *testing.T) { 132 | add := func(a, b int) int { return a + b } 133 | addVal := value.Encode(add) 134 | 135 | res, err := addVal.Call( 136 | value.Int(15), 137 | value.Int(43), 138 | ) 139 | require.NoError(t, err) 140 | require.Equal(t, int64(15+43), res.Int()) 141 | }) 142 | 143 | t.Run("fully variadic", func(t *testing.T) { 144 | add := func(nums ...int) int { 145 | var sum int 146 | for _, num := range nums { 147 | sum += num 148 | } 149 | return sum 150 | } 151 | addVal := value.Encode(add) 152 | 153 | t.Run("no args", func(t *testing.T) { 154 | res, err := addVal.Call() 155 | require.NoError(t, err) 156 | require.Equal(t, int64(0), res.Int()) 157 | }) 158 | 159 | t.Run("one arg", func(t *testing.T) { 160 | res, err := addVal.Call(value.Int(32)) 161 | require.NoError(t, err) 162 | require.Equal(t, int64(32), res.Int()) 163 | }) 164 | 165 | t.Run("many args", func(t *testing.T) { 166 | res, err := addVal.Call( 167 | value.Int(32), 168 | value.Int(59), 169 | value.Int(12), 170 | ) 171 | require.NoError(t, err) 172 | require.Equal(t, int64(32+59+12), res.Int()) 173 | }) 174 | }) 175 | 176 | t.Run("partially variadic", func(t *testing.T) { 177 | add := func(firstNum int, nums ...int) int { 178 | sum := firstNum 179 | for _, num := range nums { 180 | sum += num 181 | } 182 | return sum 183 | } 184 | addVal := value.Encode(add) 185 | 186 | t.Run("no variadic args", func(t *testing.T) { 187 | res, err := addVal.Call(value.Int(52)) 188 | require.NoError(t, err) 189 | require.Equal(t, int64(52), res.Int()) 190 | }) 191 | 192 | t.Run("one variadic arg", func(t *testing.T) { 193 | res, err := addVal.Call(value.Int(52), value.Int(32)) 194 | require.NoError(t, err) 195 | require.Equal(t, int64(52+32), res.Int()) 196 | }) 197 | 198 | t.Run("many variadic args", func(t *testing.T) { 199 | res, err := addVal.Call( 200 | value.Int(32), 201 | value.Int(59), 202 | value.Int(12), 203 | ) 204 | require.NoError(t, err) 205 | require.Equal(t, int64(32+59+12), res.Int()) 206 | }) 207 | }) 208 | 209 | t.Run("returns error", func(t *testing.T) { 210 | failWhenTrue := func(val bool) (int, error) { 211 | if val { 212 | return 0, fmt.Errorf("function failed for a very good reason") 213 | } 214 | return 0, nil 215 | } 216 | funcVal := value.Encode(failWhenTrue) 217 | 218 | t.Run("no error", func(t *testing.T) { 219 | res, err := funcVal.Call(value.Bool(false)) 220 | require.NoError(t, err) 221 | require.Equal(t, int64(0), res.Int()) 222 | }) 223 | 224 | t.Run("error", func(t *testing.T) { 225 | _, err := funcVal.Call(value.Bool(true)) 226 | require.EqualError(t, err, "function failed for a very good reason") 227 | }) 228 | }) 229 | } 230 | 231 | func TestValue_Interface_In_Array(t *testing.T) { 232 | type Container struct { 233 | Field io.Closer `river:"field,attr"` 234 | } 235 | 236 | val := value.Encode(Container{Field: io.NopCloser(nil)}) 237 | fieldVal, ok := val.Key("field") 238 | require.True(t, ok, "field not found in object") 239 | require.Equal(t, value.TypeCapsule, fieldVal.Type()) 240 | 241 | arrVal := value.Array(fieldVal) 242 | require.Equal(t, value.TypeCapsule, arrVal.Index(0).Type()) 243 | } 244 | -------------------------------------------------------------------------------- /parser/error_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/grafana/river/diag" 11 | "github.com/grafana/river/scanner" 12 | "github.com/grafana/river/token" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | // This file implements a parser test harness. The files in the testdata 18 | // directory are parsed and the errors reported are compared against the error 19 | // messages expected in the test files. 20 | // 21 | // Expected errors are indicated in the test files by putting a comment of the 22 | // form /* ERROR "rx" */ immediately following an offending token. The harness 23 | // will verify that an error matching the regular expression rx is reported at 24 | // that source position. 25 | 26 | // ERROR comments must be of the form /* ERROR "rx" */ and rx is a regular 27 | // expression that matches the expected error message. The special form 28 | // /* ERROR HERE "rx" */ must be used for error messages that appear immediately 29 | // after a token rather than at a token's position. 30 | var errRx = regexp.MustCompile(`^/\* *ERROR *(HERE)? *"([^"]*)" *\*/$`) 31 | 32 | // expectedErrors collects the regular expressions of ERROR comments found in 33 | // files and returns them as a map of error positions to error messages. 34 | func expectedErrors(file *token.File, src []byte) map[token.Pos]string { 35 | errors := make(map[token.Pos]string) 36 | 37 | s := scanner.New(file, src, nil, scanner.IncludeComments) 38 | 39 | var ( 40 | prev token.Pos // Position of last non-comment, non-terminator token 41 | here token.Pos // Position following after token at prev 42 | ) 43 | 44 | for { 45 | pos, tok, lit := s.Scan() 46 | switch tok { 47 | case token.EOF: 48 | return errors 49 | case token.COMMENT: 50 | s := errRx.FindStringSubmatch(lit) 51 | if len(s) == 3 { 52 | pos := prev 53 | if s[1] == "HERE" { 54 | pos = here 55 | } 56 | errors[pos] = s[2] 57 | } 58 | case token.TERMINATOR: 59 | if lit == "\n" { 60 | break 61 | } 62 | fallthrough 63 | default: 64 | prev = pos 65 | var l int // Token length 66 | if isLiteral(tok) { 67 | l = len(lit) 68 | } else { 69 | l = len(tok.String()) 70 | } 71 | here = prev.Add(l) 72 | } 73 | } 74 | } 75 | 76 | func isLiteral(t token.Token) bool { 77 | switch t { 78 | case token.IDENT, token.NUMBER, token.FLOAT, token.STRING: 79 | return true 80 | } 81 | return false 82 | } 83 | 84 | // compareErrors compares the map of expected error messages with the list of 85 | // found errors and reports mismatches. 86 | func compareErrors(t *testing.T, file *token.File, expected map[token.Pos]string, found diag.Diagnostics) { 87 | t.Helper() 88 | 89 | for _, checkError := range found { 90 | pos := file.Pos(checkError.StartPos.Offset) 91 | 92 | if msg, found := expected[pos]; found { 93 | // We expect a message at pos; check if it matches 94 | rx, err := regexp.Compile(msg) 95 | if !assert.NoError(t, err) { 96 | continue 97 | } 98 | assert.True(t, 99 | rx.MatchString(checkError.Message), 100 | "%s: %q does not match %q", 101 | checkError.StartPos, checkError.Message, msg, 102 | ) 103 | delete(expected, pos) // Eliminate consumed error 104 | } else { 105 | assert.Fail(t, 106 | "Unexpected error", 107 | "unexpected error: %s: %s", checkError.StartPos.String(), checkError.Message, 108 | ) 109 | } 110 | } 111 | 112 | // There should be no expected errors left 113 | if len(expected) > 0 { 114 | t.Errorf("%d errors not reported:", len(expected)) 115 | for pos, msg := range expected { 116 | t.Errorf("%s: %s\n", file.PositionFor(pos), msg) 117 | } 118 | } 119 | } 120 | 121 | func TestErrors(t *testing.T) { 122 | list, err := os.ReadDir("testdata") 123 | require.NoError(t, err) 124 | 125 | for _, d := range list { 126 | name := d.Name() 127 | if d.IsDir() || !strings.HasSuffix(name, ".river") { 128 | continue 129 | } 130 | 131 | t.Run(name, func(t *testing.T) { 132 | checkErrors(t, filepath.Join("testdata", name)) 133 | }) 134 | } 135 | } 136 | 137 | func checkErrors(t *testing.T, filename string) { 138 | t.Helper() 139 | 140 | src, err := os.ReadFile(filename) 141 | require.NoError(t, err) 142 | 143 | p := newParser(filename, src) 144 | _ = p.ParseFile() 145 | 146 | expected := expectedErrors(p.file, src) 147 | compareErrors(t, p.file, expected, p.diags) 148 | } 149 | -------------------------------------------------------------------------------- /parser/internal_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestObjectFieldName(t *testing.T) { 10 | tt := []string{ 11 | `field_a = 5`, 12 | `"field_a" = 5`, // Quotes should be removed from the field name 13 | } 14 | 15 | for _, tc := range tt { 16 | p := newParser(t.Name(), []byte(tc)) 17 | 18 | res := p.parseField() 19 | 20 | assert.Equal(t, "field_a", res.Name.Name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | // Package parser implements utilities for parsing River configuration files. 2 | package parser 3 | 4 | import ( 5 | "github.com/grafana/river/ast" 6 | "github.com/grafana/river/token" 7 | ) 8 | 9 | // ParseFile parses an entire River configuration file. The data parameter 10 | // should hold the file contents to parse, while the filename parameter is used 11 | // for reporting errors. 12 | // 13 | // If an error was encountered during parsing, the returned AST will be nil and 14 | // err will be an diag.Diagnostics all the errors encountered during parsing. 15 | func ParseFile(filename string, data []byte) (*ast.File, error) { 16 | p := newParser(filename, data) 17 | 18 | f := p.ParseFile() 19 | if len(p.diags) > 0 { 20 | return nil, p.diags 21 | } 22 | return f, nil 23 | } 24 | 25 | // ParseExpression parses a single River expression from expr. 26 | // 27 | // If an error was encountered during parsing, the returned expression will be 28 | // nil and err will be an ErrorList with all the errors encountered during 29 | // parsing. 30 | func ParseExpression(expr string) (ast.Expr, error) { 31 | p := newParser("", []byte(expr)) 32 | 33 | e := p.ParseExpression() 34 | 35 | // If the current token is not a TERMINATOR then the parsing did not complete 36 | // in full and there are still parts of the string left unparsed. 37 | p.expect(token.TERMINATOR) 38 | 39 | if len(p.diags) > 0 { 40 | return nil, p.diags 41 | } 42 | return e, nil 43 | } 44 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func FuzzParser(f *testing.F) { 13 | filepath.WalkDir("./testdata/valid", func(path string, d fs.DirEntry, _ error) error { 14 | if d.IsDir() { 15 | return nil 16 | } 17 | 18 | bb, err := os.ReadFile(path) 19 | require.NoError(f, err) 20 | f.Add(bb) 21 | return nil 22 | }) 23 | 24 | f.Fuzz(func(t *testing.T, input []byte) { 25 | p := newParser(t.Name(), input) 26 | 27 | _ = p.ParseFile() 28 | if len(p.diags) > 0 { 29 | t.SkipNow() 30 | } 31 | }) 32 | } 33 | 34 | // TestValid parses every *.river file in testdata, which is expected to be 35 | // valid. 36 | func TestValid(t *testing.T) { 37 | filepath.WalkDir("./testdata/valid", func(path string, d fs.DirEntry, _ error) error { 38 | if d.IsDir() { 39 | return nil 40 | } 41 | 42 | t.Run(filepath.Base(path), func(t *testing.T) { 43 | bb, err := os.ReadFile(path) 44 | require.NoError(t, err) 45 | 46 | p := newParser(path, bb) 47 | 48 | res := p.ParseFile() 49 | require.NotNil(t, res) 50 | require.Len(t, p.diags, 0) 51 | }) 52 | 53 | return nil 54 | }) 55 | } 56 | 57 | func TestParseExpressions(t *testing.T) { 58 | tt := map[string]string{ 59 | "literal number": `10`, 60 | "literal float": `15.0`, 61 | "literal string": `"Hello, world!"`, 62 | "literal ident": `some_ident`, 63 | "literal null": `null`, 64 | "literal true": `true`, 65 | "literal false": `false`, 66 | 67 | "empty array": `[]`, 68 | "array one element": `[1]`, 69 | "array many elements": `[0, 1, 2, 3]`, 70 | "array trailing comma": `[0, 1, 2, 3,]`, 71 | "nested array": `[[0, 1, 2], [3, 4, 5]]`, 72 | "array multiline": `[ 73 | 0, 74 | 1, 75 | 2, 76 | ]`, 77 | 78 | "empty object": `{}`, 79 | "object one field": `{ field_a = 5 }`, 80 | "object multiple fields": `{ field_a = 5, field_b = 10 }`, 81 | "object trailing comma": `{ field_a = 5, field_b = 10, }`, 82 | "nested objects": `{ field_a = { nested_field = 100 } }`, 83 | "object multiline": `{ 84 | field_a = 5, 85 | field_b = 10, 86 | }`, 87 | 88 | "unary not": `!true`, 89 | "unary neg": `-5`, 90 | 91 | "math": `1 + 2 - 3 * 4 / 5 % 6`, 92 | "compare ops": `1 == 2 != 3 < 4 > 5 <= 6 >= 7`, 93 | "logical ops": `true || false && true`, 94 | "pow operator": "1 ^ 2 ^ 3", 95 | 96 | "field access": `a.b.c.d`, 97 | "element access": `a[0][1][2]`, 98 | 99 | "call no args": `a()`, 100 | "call one arg": `a(1)`, 101 | "call multiple args": `a(1,2,3)`, 102 | "call with trailing comma": `a(1,2,3,)`, 103 | "call multiline": `a( 104 | 1, 105 | 2, 106 | 3, 107 | )`, 108 | 109 | "parens": `(1 + 5) * 100`, 110 | 111 | "mixed expression": `(a.b.c)(1, 3 * some_list[magic_index * 2]).resulting_field`, 112 | } 113 | 114 | for name, input := range tt { 115 | t.Run(name, func(t *testing.T) { 116 | p := newParser(name, []byte(input)) 117 | 118 | res := p.ParseExpression() 119 | require.NotNil(t, res) 120 | require.Len(t, p.diags, 0) 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /parser/testdata/assign_block_to_attr.river: -------------------------------------------------------------------------------- 1 | rw = prometheus/* ERROR "cannot use a block as an expression" */.remote_write "default" { 2 | endpoint { 3 | url = "some_url" 4 | basic_auth { 5 | username = "username" 6 | password = "password" 7 | } 8 | } 9 | } 10 | 11 | attr_1 = 15 12 | attr_2 = 51 13 | 14 | block { 15 | rw_2 = prometheus/* ERROR "cannot use a block as an expression" */.remote_write "other" { 16 | endpoint { 17 | url = "other_url" 18 | basic_auth { 19 | username = "username_2" 20 | password = "password_2" 21 | } 22 | } 23 | } 24 | } 25 | 26 | other_block { 27 | // This is an expression which looks like it might be a block at first, but 28 | // then isn't. 29 | rw_3 = prometheus.remote_write "other" "other" /* ERROR "expected {, got STRING" */ 12345 30 | } 31 | 32 | attr_3 = 15 33 | -------------------------------------------------------------------------------- /parser/testdata/attribute_names.river: -------------------------------------------------------------------------------- 1 | valid_attr = 15 2 | 3 | // The parser parses block names for both blocks and attributes, and later 4 | // validates that the attribute name is just a single identifier with no label. 5 | 6 | invalid/* ERROR "attribute names may only consist of a single identifier" */.attr = 20 7 | invalid "label" /* ERROR "attribute names may not have labels" */ = 20 8 | -------------------------------------------------------------------------------- /parser/testdata/block_names.river: -------------------------------------------------------------------------------- 1 | valid_block { 2 | 3 | } 4 | 5 | valid_block "labeled" { 6 | 7 | } 8 | 9 | invalid_block bad_label_name /* ERROR "expected block label, got IDENT" */ { 10 | 11 | } 12 | 13 | other_valid_block { 14 | nested_block { 15 | 16 | } 17 | 18 | nested_block "labeled" { 19 | 20 | } 21 | } 22 | 23 | invalid_block "with space" /* ERROR "expected block label to be a valid identifier" */ { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /parser/testdata/commas.river: -------------------------------------------------------------------------------- 1 | // Test that missing trailing commas for multiline expressions get reported. 2 | 3 | field = [ 4 | 0, 5 | 1, 6 | 2/* ERROR HERE "missing ',' in expression list" */ 7 | ] 8 | 9 | obj = { 10 | field_a = 0, 11 | field_b = 1, 12 | field_c = 2/* ERROR HERE "missing ',' in field list" */ 13 | } 14 | -------------------------------------------------------------------------------- /parser/testdata/fuzz/FuzzParser/1a39f4e358facc21678b16fad53537b46efdaa76e024a5ef4955d01a68bdac37: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("A0000000000000000") 3 | -------------------------------------------------------------------------------- /parser/testdata/fuzz/FuzzParser/248cf4391f6c48550b7d2cf4c6c80f4ba9099c21ffa2b6869e75e99565dce037: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("A={A!0A\"") 3 | -------------------------------------------------------------------------------- /parser/testdata/fuzz/FuzzParser/b919fa00ebca318001778477c839a06204b55f2636597901d8d7878150d8580a: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("A\"") 3 | -------------------------------------------------------------------------------- /parser/testdata/invalid_exprs.river: -------------------------------------------------------------------------------- 1 | attr = 1 + + /* ERROR "expected expression, got +" */ 2 2 | 3 | invalid_func_call = a(() /* ERROR "expected expression, got \)" */) 4 | invalid_access = a.true /* ERROR "expected IDENT, got BOOL" */ 5 | -------------------------------------------------------------------------------- /parser/testdata/invalid_object_key.river: -------------------------------------------------------------------------------- 1 | obj { 2 | map = { 3 | "string_field" = "foo", 4 | identifier_string = "bar", 5 | 1337 /* ERROR "expected field name \(string or identifier\), got NUMBER" */ = "baz", 6 | "another_field" = "qux", 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /parser/testdata/valid/attribute.river: -------------------------------------------------------------------------------- 1 | number_field = 1 2 | -------------------------------------------------------------------------------- /parser/testdata/valid/blocks.river: -------------------------------------------------------------------------------- 1 | one_ident { 2 | number_field = 1 3 | } 4 | 5 | one_ident "labeled" { 6 | number_field = 1 7 | } 8 | 9 | multiple.idents { 10 | number_field = 1 11 | } 12 | 13 | multiple.idents "labeled" { 14 | number_field = 1 15 | } 16 | 17 | chain.of.idents { 18 | number_field = 1 19 | } 20 | 21 | chain.of.idents "labeled" { 22 | number_field = 1 23 | } 24 | 25 | one_ident_inline { number_field = 1 } 26 | one_ident_inline "labeled" { number_field = 1 } 27 | multiple.idents_inline { number_field = 1 } 28 | multiple.idents_inline "labeled" { number_field = 1 } 29 | chain.of.idents { number_field = 1 } 30 | chain.of.idents "labeled" { number_field = 1 } 31 | 32 | nested_block { 33 | inner_block { 34 | some_field = true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /parser/testdata/valid/comments.river: -------------------------------------------------------------------------------- 1 | // Hello, world! 2 | -------------------------------------------------------------------------------- /parser/testdata/valid/empty.river: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/river/8e092d39219ca64d9a808f7155550baa5f262388/parser/testdata/valid/empty.river -------------------------------------------------------------------------------- /parser/testdata/valid/expressions.river: -------------------------------------------------------------------------------- 1 | // Literals 2 | lit_number = 10 3 | lit_float = 15.0 4 | lit_string = "Hello, world!" 5 | lit_ident = other_ident 6 | lit_null = null 7 | lit_true = true 8 | lit_false = false 9 | 10 | // Arrays 11 | array_expr_empty = [] 12 | array_expr_one_element = [0] 13 | array_expr = [0, 1, 2, 3] 14 | array_expr_trailing = [0, 1, 2, 3,] 15 | array_expr_multiline = [ 16 | 0, 17 | 1, 18 | 2, 19 | 3, 20 | ] 21 | array_expr_nested = [[1]] 22 | 23 | // Objects 24 | object_expr_empty = {} 25 | object_expr_one_field = { field_a = 1 } 26 | object_expr = { field_a = 1, field_b = 2 } 27 | object_expr_trailing = { field_a = 1, field_b = 2, } 28 | object_expr_multiline = { 29 | field_a = 1, 30 | field_b = 2, 31 | } 32 | object_expr_nested = { field_a = { nested_field_a = 1 } } 33 | 34 | // Unary ops 35 | not_something = !true 36 | neg_number = -5 37 | 38 | // Math binops 39 | binop_sum = 1 + 2 40 | binop_sub = 1 - 2 41 | binop_mul = 1 * 2 42 | binop_div = 1 / 2 43 | binop_mod = 1 % 2 44 | binop_pow = 1 ^ 2 ^ 3 45 | 46 | // Compare binops 47 | binop_eq = 1 == 2 48 | binop_neq = 1 != 2 49 | binop_lt = 1 < 2 50 | binop_lte = 1 <= 2 51 | binop_gt = 1 > 2 52 | binop_gte = 1 >= 2 53 | 54 | // Logical binops 55 | binop_or = true || false 56 | binop_and = true && false 57 | 58 | 59 | // Mixed math operations 60 | math = 1 + 2 - 3 * 4 / 5 % 6 61 | compare_ops = 1 == 2 != 3 < 4 > 5 <= 6 >= 7 62 | logical_ops = true || false && true 63 | mixed_assoc = 1 * 3 + 5 ^ 3 - 2 % 1 // Test with both left- and right- associative operators 64 | expr_parens = (5 * 2) + 5 65 | 66 | // Accessors 67 | field_access = a.b.c.d 68 | element_access = a[0][1][2] 69 | 70 | // Function calls 71 | call_no_args = a() 72 | call_one_arg = a(1) 73 | call_multiple_args = a(1,2,3) 74 | call_trailing_comma = a(1,2,3,) 75 | call_multiline = a( 76 | 1, 77 | 2, 78 | 3, 79 | ) 80 | 81 | mixed_expr = (a.b.c)(1, 3 * some_list[magic_index * 2]).resulting_field 82 | -------------------------------------------------------------------------------- /printer/printer_test.go: -------------------------------------------------------------------------------- 1 | package printer_test 2 | 3 | import ( 4 | "bytes" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | "unicode" 11 | 12 | "github.com/grafana/river/parser" 13 | "github.com/grafana/river/printer" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestPrinter(t *testing.T) { 18 | filepath.WalkDir("testdata", func(path string, d fs.DirEntry, _ error) error { 19 | if d.IsDir() { 20 | return nil 21 | } 22 | 23 | if strings.HasSuffix(path, ".in") { 24 | inputFile := path 25 | expectFile := strings.TrimSuffix(path, ".in") + ".expect" 26 | expectErrorFile := strings.TrimSuffix(path, ".in") + ".error" 27 | 28 | caseName := filepath.Base(path) 29 | caseName = strings.TrimSuffix(caseName, ".in") 30 | 31 | t.Run(caseName, func(t *testing.T) { 32 | testPrinter(t, inputFile, expectFile, expectErrorFile) 33 | }) 34 | } 35 | 36 | return nil 37 | }) 38 | } 39 | 40 | func testPrinter(t *testing.T, inputFile string, expectFile string, expectErrorFile string) { 41 | inputBB, err := os.ReadFile(inputFile) 42 | require.NoError(t, err) 43 | 44 | f, err := parser.ParseFile(t.Name()+".rvr", inputBB) 45 | if expectedError := getExpectedErrorMessage(t, expectErrorFile); expectedError != "" { 46 | require.Error(t, err) 47 | require.Contains(t, err.Error(), expectedError) 48 | return 49 | } 50 | 51 | expectBB, err := os.ReadFile(expectFile) 52 | require.NoError(t, err) 53 | 54 | var buf bytes.Buffer 55 | require.NoError(t, printer.Fprint(&buf, f)) 56 | 57 | trimmed := strings.TrimRightFunc(string(expectBB), unicode.IsSpace) 58 | require.Equal(t, trimmed, buf.String(), "%s", buf.String()) 59 | } 60 | 61 | // getExpectedErrorMessage will retrieve an optional expected error message for the test. 62 | func getExpectedErrorMessage(t *testing.T, errorFile string) string { 63 | if _, err := os.Stat(errorFile); err == nil { 64 | errorBytes, err := os.ReadFile(errorFile) 65 | require.NoError(t, err) 66 | errorsString := string(normalizeLineEndings(errorBytes)) 67 | return errorsString 68 | } 69 | 70 | return "" 71 | } 72 | 73 | // normalizeLineEndings will replace '\r\n' with '\n'. 74 | func normalizeLineEndings(data []byte) []byte { 75 | normalized := bytes.ReplaceAll(data, []byte{'\r', '\n'}, []byte{'\n'}) 76 | return normalized 77 | } 78 | -------------------------------------------------------------------------------- /printer/testdata/.gitattributes: -------------------------------------------------------------------------------- 1 | * -text eol=lf 2 | -------------------------------------------------------------------------------- /printer/testdata/array_comments.expect: -------------------------------------------------------------------------------- 1 | // array_comments.in expects that comments in arrays are formatted to 2 | // retain the indentation level of elements within the arrays. 3 | 4 | attr = [ // Inline comment 5 | 0, 1, 2, // Inline comment 6 | 3, 4, 5, // Inline comment 7 | // Trailing comment 8 | ] 9 | 10 | attr = [ 11 | 0, 12 | // Element-level comment 13 | 1, 14 | // Element-level comment 15 | 2, 16 | // Trailing comment 17 | ] 18 | -------------------------------------------------------------------------------- /printer/testdata/array_comments.in: -------------------------------------------------------------------------------- 1 | // array_comments.in expects that comments in arrays are formatted to 2 | // retain the indentation level of elements within the arrays. 3 | 4 | attr = [ // Inline comment 5 | 0, 1, 2, // Inline comment 6 | 3, 4, 5, // Inline comment 7 | // Trailing comment 8 | ] 9 | 10 | attr = [ 11 | 0, 12 | // Element-level comment 13 | 1, 14 | // Element-level comment 15 | 2, 16 | // Trailing comment 17 | ] 18 | -------------------------------------------------------------------------------- /printer/testdata/block_comments.expect: -------------------------------------------------------------------------------- 1 | // block_comments.in expects that comments within blocks are formatted to 2 | // remain within the block with the proper indentation. 3 | 4 | // 5 | // Unlabeled blocks 6 | // 7 | 8 | // Comment is on same line as empty block header. 9 | block { // comment 10 | } 11 | 12 | // Comment is on same line as non-empty block header. 13 | block { // comment 14 | attr = 5 15 | } 16 | 17 | // Comment is alone in block body. 18 | block { 19 | // comment 20 | } 21 | 22 | // Comment is before a statement. 23 | block { 24 | // comment 25 | attr = 5 26 | } 27 | 28 | // Comment is after a statement. 29 | block { 30 | attr = 5 31 | // comment 32 | } 33 | 34 | // 35 | // Labeled blocks 36 | // 37 | 38 | // Comment is on same line as empty block header. 39 | block "label" { // comment 40 | } 41 | 42 | // Comment is on same line as non-empty block header. 43 | block "label" { // comment 44 | attr = 5 45 | } 46 | 47 | // Comment is alone in block body. 48 | block "label" { 49 | // comment 50 | } 51 | 52 | // Comment is before a statement. 53 | block "label" { 54 | // comment 55 | attr = 5 56 | } 57 | 58 | // Comment is after a statement. 59 | block "label" { 60 | attr = 5 61 | // comment 62 | } 63 | -------------------------------------------------------------------------------- /printer/testdata/block_comments.in: -------------------------------------------------------------------------------- 1 | // block_comments.in expects that comments within blocks are formatted to 2 | // remain within the block with the proper indentation. 3 | 4 | // 5 | // Unlabeled blocks 6 | // 7 | 8 | // Comment is on same line as empty block header. 9 | block { // comment 10 | } 11 | 12 | // Comment is on same line as non-empty block header. 13 | block { // comment 14 | attr = 5 15 | } 16 | 17 | // Comment is alone in block body. 18 | block { 19 | // comment 20 | } 21 | 22 | // Comment is before a statement. 23 | block { 24 | // comment 25 | attr = 5 26 | } 27 | 28 | // Comment is after a statement. 29 | block { 30 | attr = 5 31 | // comment 32 | } 33 | 34 | // 35 | // Labeled blocks 36 | // 37 | 38 | // Comment is on same line as empty block header. 39 | block "label" { // comment 40 | } 41 | 42 | // Comment is on same line as non-empty block header. 43 | block "label" { // comment 44 | attr = 5 45 | } 46 | 47 | // Comment is alone in block body. 48 | block "label" { 49 | // comment 50 | } 51 | 52 | // Comment is before a statement. 53 | block "label" { 54 | // comment 55 | attr = 5 56 | } 57 | 58 | // Comment is after a statement. 59 | block "label" { 60 | attr = 5 61 | // comment 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /printer/testdata/example.expect: -------------------------------------------------------------------------------- 1 | // This file tests a little bit of everything that the formatter should do. For 2 | // example, this block of comments itself ensures that the output retains 3 | // comments found in the source file. 4 | 5 | // 6 | // Whitespace tests 7 | // 8 | 9 | // Attributes should be given whitespace 10 | attr_1 = 15 11 | attr_2 = 30 * 2 + 5 12 | attr_3 = field.access * 2 13 | 14 | // Blocks with nothing inside of them should be truncated. 15 | empty.block { } 16 | 17 | empty.block "labeled" { } 18 | 19 | // 20 | // Alignment tests 21 | // 22 | 23 | // Sequences of attributes which aren't separated by a blank line should have 24 | // the equal sign aligned. 25 | short_name = true 26 | really_long_name = true 27 | 28 | extremely_long_name = true 29 | 30 | // Sequences of comments on aligned lines should also be aligned. 31 | short_name = "short value" // Align me 32 | really_long_name = "really long value" // Align me 33 | 34 | extremely_long_name = true // Unaligned 35 | 36 | // 37 | // Indentation tests 38 | // 39 | 40 | // Array literals, object literals, and blocks should all be indented properly. 41 | multiline_array = [ 42 | 0, 43 | 1, 44 | ] 45 | 46 | mulitiline_object = { 47 | foo = "bar", 48 | } 49 | 50 | some_block { 51 | attr = 15 52 | 53 | inner_block { 54 | attr = 20 55 | } 56 | } 57 | 58 | // Trailing comments should be retained in the output. If this comment gets 59 | // trimmed out, it usually indicates that a final flush is missing after 60 | // traversing the AST. 61 | -------------------------------------------------------------------------------- /printer/testdata/example.in: -------------------------------------------------------------------------------- 1 | // This file tests a little bit of everything that the formatter should do. For 2 | // example, this block of comments itself ensures that the output retains 3 | // comments found in the source file. 4 | 5 | // 6 | // Whitespace tests 7 | // 8 | 9 | // Attributes should be given whitespace 10 | attr_1=15 11 | attr_2=30*2+5 12 | attr_3=field.access*2 13 | 14 | // Blocks with nothing inside of them should be truncated. 15 | empty.block { 16 | 17 | } 18 | 19 | empty.block "labeled" { 20 | 21 | } 22 | 23 | // 24 | // Alignment tests 25 | // 26 | 27 | // Sequences of attributes which aren't separated by a blank line should have 28 | // the equal sign aligned. 29 | short_name = true 30 | really_long_name = true 31 | 32 | extremely_long_name = true 33 | 34 | // Sequences of comments on aligned lines should also be aligned. 35 | short_name = "short value" // Align me 36 | really_long_name = "really long value" // Align me 37 | 38 | extremely_long_name = true // Unaligned 39 | 40 | // 41 | // Indentation tests 42 | // 43 | 44 | // Array literals, object literals, and blocks should all be indented properly. 45 | multiline_array = [ 46 | 0, 47 | 1, 48 | ] 49 | 50 | mulitiline_object = { 51 | foo = "bar", 52 | } 53 | 54 | some_block { 55 | attr = 15 56 | 57 | inner_block { 58 | attr = 20 59 | } 60 | } 61 | 62 | // Trailing comments should be retained in the output. If this comment gets 63 | // trimmed out, it usually indicates that a final flush is missing after 64 | // traversing the AST. 65 | -------------------------------------------------------------------------------- /printer/testdata/func_call.expect: -------------------------------------------------------------------------------- 1 | one_line = some_func(1, 2, 3, 4) 2 | 3 | multi_line = some_func(1, 4 | 2, 3, 5 | 4) 6 | 7 | multi_line_pretty = some_func( 8 | 1, 9 | 2, 10 | 3, 11 | 4, 12 | ) 13 | 14 | func_with_obj = some_func({ 15 | key1 = "value1", 16 | key2 = "value2", 17 | }) 18 | -------------------------------------------------------------------------------- /printer/testdata/func_call.in: -------------------------------------------------------------------------------- 1 | one_line = some_func(1, 2, 3, 4) 2 | 3 | multi_line = some_func(1, 4 | 2, 3, 5 | 4) 6 | 7 | multi_line_pretty = some_func( 8 | 1, 9 | 2, 10 | 3, 11 | 4, 12 | ) 13 | 14 | func_with_obj = some_func({ 15 | key1 = "value1", 16 | key2 = "value2", 17 | }) 18 | -------------------------------------------------------------------------------- /printer/testdata/mixed_list.expect: -------------------------------------------------------------------------------- 1 | mixed_list = [0, true, { 2 | key_1 = true, 3 | key_2 = true, 4 | key_3 = true, 5 | }, "Hello!"] 6 | 7 | mixed_list_2 = [ 8 | 0, 9 | true, 10 | { 11 | key_1 = true, 12 | key_2 = true, 13 | key_3 = true, 14 | }, 15 | "Hello!", 16 | ] 17 | -------------------------------------------------------------------------------- /printer/testdata/mixed_list.in: -------------------------------------------------------------------------------- 1 | mixed_list = [0, true, { 2 | key_1 = true, 3 | key_2 = true, 4 | key_3 = true, 5 | }, "Hello!"] 6 | 7 | mixed_list_2 = [ 8 | 0, 9 | true, 10 | { 11 | key_1 = true, 12 | key_2 = true, 13 | key_3 = true, 14 | }, 15 | "Hello!", 16 | ] 17 | -------------------------------------------------------------------------------- /printer/testdata/mixed_object.expect: -------------------------------------------------------------------------------- 1 | mixed_object = { 2 | key_1 = true, 3 | key_2 = [0, true, { 4 | inner_1 = true, 5 | inner_2 = true, 6 | }], 7 | } 8 | 9 | -------------------------------------------------------------------------------- /printer/testdata/mixed_object.in: -------------------------------------------------------------------------------- 1 | mixed_object = { 2 | key_1 = true, 3 | key_2 = [0, true, { 4 | inner_1 = true, 5 | inner_2 = true, 6 | }], 7 | } 8 | -------------------------------------------------------------------------------- /printer/testdata/object_align.expect: -------------------------------------------------------------------------------- 1 | block { 2 | some_object = { 3 | key_1 = 5, 4 | long_key = 10, 5 | longer_key = { 6 | inner_key = true, 7 | inner_key_2 = false, 8 | }, 9 | other_key = [0, 1, 2], 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /printer/testdata/object_align.in: -------------------------------------------------------------------------------- 1 | block { 2 | some_object = { 3 | key_1 = 5, 4 | long_key = 10, 5 | longer_key = { 6 | inner_key = true, 7 | inner_key_2 = false, 8 | }, 9 | other_key = [0, 1, 2], 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /printer/testdata/oneline_block.expect: -------------------------------------------------------------------------------- 1 | block { } 2 | 3 | block { } 4 | 5 | block { } 6 | 7 | block { } 8 | 9 | block { 10 | // Comments should be kept. 11 | } 12 | -------------------------------------------------------------------------------- /printer/testdata/oneline_block.in: -------------------------------------------------------------------------------- 1 | block {} 2 | 3 | block { } 4 | 5 | block { 6 | } 7 | 8 | block { 9 | 10 | } 11 | 12 | block { 13 | // Comments should be kept. 14 | } 15 | -------------------------------------------------------------------------------- /printer/testdata/raw_string.expect: -------------------------------------------------------------------------------- 1 | block "label" { 2 | attr = `'\"attr` 3 | } 4 | 5 | block "multi_line" { 6 | attr = `'\"this 7 | is 8 | a 9 | multi_line 10 | attr'\"` 11 | } 12 | 13 | block "json" { 14 | attr = `{ "key": "value" }` 15 | } -------------------------------------------------------------------------------- /printer/testdata/raw_string.in: -------------------------------------------------------------------------------- 1 | block "label" { 2 | attr = `'\"attr` 3 | } 4 | 5 | block "multi_line" { 6 | attr = `'\"this 7 | is 8 | a 9 | multi_line 10 | attr'\"` 11 | } 12 | 13 | block "json" { 14 | attr = `{ "key": "value" }` 15 | } -------------------------------------------------------------------------------- /printer/testdata/raw_string_label_error.error: -------------------------------------------------------------------------------- 1 | expected block label to be a double quoted string, but got "`multi_line`" -------------------------------------------------------------------------------- /printer/testdata/raw_string_label_error.in: -------------------------------------------------------------------------------- 1 | block "label" { 2 | attr = `'\"attr` 3 | } 4 | 5 | block `multi_line` { 6 | attr = `'\"this 7 | is 8 | a 9 | multi_line 10 | attr'\"` 11 | } 12 | 13 | block `json` { 14 | attr = `{ "key": "value" }` 15 | } -------------------------------------------------------------------------------- /printer/trimmer.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "io" 5 | "text/tabwriter" 6 | ) 7 | 8 | // A trimmer is an io.Writer which filters tabwriter.Escape characters, 9 | // trailing blanks and tabs from lines, and converting \f and \v characters 10 | // into \n and \t (if no text/tabwriter is used when printing). 11 | // 12 | // Text wrapped by tabwriter.Escape characters is written to the underlying 13 | // io.Writer unmodified. 14 | type trimmer struct { 15 | next io.Writer 16 | state int 17 | space []byte 18 | } 19 | 20 | const ( 21 | trimStateSpace = iota // Trimmer is reading space characters 22 | trimStateEscape // Trimmer is reading escaped characters 23 | trimStateText // Trimmer is reading text 24 | ) 25 | 26 | func (t *trimmer) discardWhitespace() { 27 | t.state = trimStateSpace 28 | t.space = t.space[0:0] 29 | } 30 | 31 | func (t *trimmer) Write(data []byte) (n int, err error) { 32 | // textStart holds the index of the start of a chunk of text not containing 33 | // whitespace. It is reset every time a new chunk of text is encountered. 34 | var textStart int 35 | 36 | for off, b := range data { 37 | // Convert \v to \t 38 | if b == '\v' { 39 | b = '\t' 40 | } 41 | 42 | switch t.state { 43 | case trimStateSpace: 44 | // Accumulate tabs and spaces in t.space until finding a non-tab or 45 | // non-space character. 46 | // 47 | // If we find a newline, we write it directly and discard our pending 48 | // whitespace (so that trailing whitespace up to the newline is ignored). 49 | // 50 | // If we find a tabwriter.Escape or text character we transition states. 51 | switch b { 52 | case '\t', ' ': 53 | t.space = append(t.space, b) 54 | case '\n', '\f': 55 | // Discard all unwritten whitespace before the end of the line and write 56 | // a newline. 57 | t.discardWhitespace() 58 | _, err = t.next.Write([]byte("\n")) 59 | case tabwriter.Escape: 60 | _, err = t.next.Write(t.space) 61 | t.state = trimStateEscape 62 | textStart = off + 1 // Skip escape character 63 | default: 64 | // Non-space character. Write our pending whitespace 65 | // and then move to text state. 66 | _, err = t.next.Write(t.space) 67 | t.state = trimStateText 68 | textStart = off 69 | } 70 | 71 | case trimStateText: 72 | // We're reading a chunk of text. Accumulate characters in the chunk 73 | // until we find a whitespace character or a tabwriter.Escape. 74 | switch b { 75 | case '\t', ' ': 76 | _, err = t.next.Write(data[textStart:off]) 77 | t.discardWhitespace() 78 | t.space = append(t.space, b) 79 | case '\n', '\f': 80 | _, err = t.next.Write(data[textStart:off]) 81 | t.discardWhitespace() 82 | if err == nil { 83 | _, err = t.next.Write([]byte("\n")) 84 | } 85 | case tabwriter.Escape: 86 | _, err = t.next.Write(data[textStart:off]) 87 | t.state = trimStateEscape 88 | textStart = off + 1 // +1: skip tabwriter.Escape 89 | } 90 | 91 | case trimStateEscape: 92 | // Accumulate everything until finding the closing tabwriter.Escape. 93 | if b == tabwriter.Escape { 94 | _, err = t.next.Write(data[textStart:off]) 95 | t.discardWhitespace() 96 | } 97 | 98 | default: 99 | panic("unreachable") 100 | } 101 | if err != nil { 102 | return off, err 103 | } 104 | } 105 | n = len(data) 106 | 107 | // Flush the remainder of the text (as long as it's not whitespace). 108 | switch t.state { 109 | case trimStateEscape, trimStateText: 110 | _, err = t.next.Write(data[textStart:n]) 111 | t.discardWhitespace() 112 | } 113 | 114 | return 115 | } 116 | -------------------------------------------------------------------------------- /river_test.go: -------------------------------------------------------------------------------- 1 | package river_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/grafana/river" 8 | ) 9 | 10 | func ExampleUnmarshal() { 11 | // Character is our block type which holds an individual character from a 12 | // book. 13 | type Character struct { 14 | // Name of the character. The name is decoded from the block label. 15 | Name string `river:",label"` 16 | // Age of the character. The age is a required attribute within the block, 17 | // and must be set in the config. 18 | Age int `river:"age,attr"` 19 | // Location the character lives in. The location is an optional attribute 20 | // within the block. Optional attributes do not have to bet set. 21 | Location string `river:"location,attr,optional"` 22 | } 23 | 24 | // Book is our overall type where we decode the overall River file into. 25 | type Book struct { 26 | // Title of the book (required attribute). 27 | Title string `river:"title,attr"` 28 | // List of characters. Each character is a labeled block. The optional tag 29 | // means that it is valid not provide a character block. Decoding into a 30 | // slice permits there to be multiple specified character blocks. 31 | Characters []*Character `river:"character,block,optional"` 32 | } 33 | 34 | // Create our book with two characters. 35 | input := ` 36 | title = "Wheel of Time" 37 | 38 | character "Rand" { 39 | age = 19 40 | location = "Two Rivers" 41 | } 42 | 43 | character "Perrin" { 44 | age = 19 45 | location = "Two Rivers" 46 | } 47 | ` 48 | 49 | // Unmarshal the config into our Book type and print out the data. 50 | var b Book 51 | if err := river.Unmarshal([]byte(input), &b); err != nil { 52 | panic(err) 53 | } 54 | 55 | fmt.Printf("%s characters:\n", b.Title) 56 | 57 | for _, c := range b.Characters { 58 | if c.Location != "" { 59 | fmt.Printf("\t%s (age %d, location %s)\n", c.Name, c.Age, c.Location) 60 | } else { 61 | fmt.Printf("\t%s (age %d)\n", c.Name, c.Age) 62 | } 63 | } 64 | 65 | // Output: 66 | // Wheel of Time characters: 67 | // Rand (age 19, location Two Rivers) 68 | // Perrin (age 19, location Two Rivers) 69 | } 70 | 71 | // This example shows how functions may be called within user configurations. 72 | // We focus on the `env` function from the standard library, which retrieves a 73 | // value from an environment variable. 74 | func ExampleUnmarshal_functions() { 75 | // Set an environment variable to use in the test. 76 | _ = os.Setenv("EXAMPLE", "Jane Doe") 77 | 78 | type Data struct { 79 | String string `river:"string,attr"` 80 | } 81 | 82 | input := ` 83 | string = env("EXAMPLE") 84 | ` 85 | 86 | var d Data 87 | if err := river.Unmarshal([]byte(input), &d); err != nil { 88 | panic(err) 89 | } 90 | 91 | fmt.Println(d.String) 92 | // Output: Jane Doe 93 | } 94 | 95 | func ExampleUnmarshalValue() { 96 | input := `3 + 5` 97 | 98 | var num int 99 | if err := river.UnmarshalValue([]byte(input), &num); err != nil { 100 | panic(err) 101 | } 102 | 103 | fmt.Println(num) 104 | // Output: 8 105 | } 106 | 107 | func ExampleMarshal() { 108 | type Person struct { 109 | Name string `river:"name,attr"` 110 | Age int `river:"age,attr"` 111 | Location string `river:"location,attr,optional"` 112 | } 113 | 114 | p := Person{ 115 | Name: "John Doe", 116 | Age: 43, 117 | } 118 | 119 | bb, err := river.Marshal(p) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | fmt.Println(string(bb)) 125 | // Output: 126 | // name = "John Doe" 127 | // age = 43 128 | } 129 | 130 | func ExampleMarshalValue() { 131 | type Person struct { 132 | Name string `river:"name,attr"` 133 | Age int `river:"age,attr"` 134 | } 135 | 136 | p := Person{ 137 | Name: "John Doe", 138 | Age: 43, 139 | } 140 | 141 | bb, err := river.MarshalValue(p) 142 | if err != nil { 143 | panic(err) 144 | } 145 | 146 | fmt.Println(string(bb)) 147 | // Output: 148 | // { 149 | // name = "John Doe", 150 | // age = 43, 151 | // } 152 | } 153 | -------------------------------------------------------------------------------- /rivertypes/optional_secret.go: -------------------------------------------------------------------------------- 1 | package rivertypes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/river/internal/value" 7 | "github.com/grafana/river/token" 8 | "github.com/grafana/river/token/builder" 9 | ) 10 | 11 | // OptionalSecret holds a potentially sensitive value. When IsSecret is true, 12 | // the OptionalSecret's Value will be treated as sensitive and will be hidden 13 | // from users when rendering River. 14 | // 15 | // OptionalSecrets may be converted from river strings and the Secret type, 16 | // which will set IsSecret accordingly. 17 | // 18 | // Additionally, OptionalSecrets may be converted into the Secret type 19 | // regardless of the value of IsSecret. OptionalSecret can be converted into a 20 | // string as long as IsSecret is false. 21 | type OptionalSecret struct { 22 | IsSecret bool 23 | Value string 24 | } 25 | 26 | var ( 27 | _ value.Capsule = OptionalSecret{} 28 | _ value.ConvertibleIntoCapsule = OptionalSecret{} 29 | _ value.ConvertibleFromCapsule = (*OptionalSecret)(nil) 30 | 31 | _ builder.Tokenizer = OptionalSecret{} 32 | ) 33 | 34 | // RiverCapsule marks OptionalSecret as a RiverCapsule. 35 | func (s OptionalSecret) RiverCapsule() {} 36 | 37 | // ConvertInto converts the OptionalSecret and stores it into the Go value 38 | // pointed at by dst. OptionalSecrets can always be converted into *Secret. 39 | // OptionalSecrets can only be converted into *string if IsSecret is false. In 40 | // other cases, this method will return an explicit error or 41 | // river.ErrNoConversion. 42 | func (s OptionalSecret) ConvertInto(dst interface{}) error { 43 | switch dst := dst.(type) { 44 | case *Secret: 45 | *dst = Secret(s.Value) 46 | return nil 47 | case *string: 48 | if s.IsSecret { 49 | return fmt.Errorf("secrets may not be converted into strings") 50 | } 51 | *dst = s.Value 52 | return nil 53 | } 54 | 55 | return value.ErrNoConversion 56 | } 57 | 58 | // ConvertFrom converts the src value and stores it into the OptionalSecret s. 59 | // Secrets and strings can be converted into an OptionalSecret. In other 60 | // cases, this method will return river.ErrNoConversion. 61 | func (s *OptionalSecret) ConvertFrom(src interface{}) error { 62 | switch src := src.(type) { 63 | case Secret: 64 | *s = OptionalSecret{IsSecret: true, Value: string(src)} 65 | return nil 66 | case string: 67 | *s = OptionalSecret{Value: src} 68 | return nil 69 | } 70 | 71 | return value.ErrNoConversion 72 | } 73 | 74 | // RiverTokenize returns a set of custom tokens to represent this value in 75 | // River text. 76 | func (s OptionalSecret) RiverTokenize() []builder.Token { 77 | if s.IsSecret { 78 | return []builder.Token{{Tok: token.LITERAL, Lit: "(secret)"}} 79 | } 80 | return []builder.Token{{ 81 | Tok: token.STRING, 82 | Lit: fmt.Sprintf("%q", s.Value), 83 | }} 84 | } 85 | -------------------------------------------------------------------------------- /rivertypes/optional_secret_test.go: -------------------------------------------------------------------------------- 1 | package rivertypes_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/river/rivertypes" 7 | "github.com/grafana/river/token/builder" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestOptionalSecret(t *testing.T) { 12 | t.Run("non-sensitive conversion to string is allowed", func(t *testing.T) { 13 | input := rivertypes.OptionalSecret{IsSecret: false, Value: "testval"} 14 | 15 | var s string 16 | err := decodeTo(t, input, &s) 17 | require.NoError(t, err) 18 | require.Equal(t, "testval", s) 19 | }) 20 | 21 | t.Run("sensitive conversion to string is disallowed", func(t *testing.T) { 22 | input := rivertypes.OptionalSecret{IsSecret: true, Value: "testval"} 23 | 24 | var s string 25 | err := decodeTo(t, input, &s) 26 | require.NotNil(t, err) 27 | require.Contains(t, err.Error(), "secrets may not be converted into strings") 28 | }) 29 | 30 | t.Run("non-sensitive conversion to secret is allowed", func(t *testing.T) { 31 | input := rivertypes.OptionalSecret{IsSecret: false, Value: "testval"} 32 | 33 | var s rivertypes.Secret 34 | err := decodeTo(t, input, &s) 35 | require.NoError(t, err) 36 | require.Equal(t, rivertypes.Secret("testval"), s) 37 | }) 38 | 39 | t.Run("sensitive conversion to secret is allowed", func(t *testing.T) { 40 | input := rivertypes.OptionalSecret{IsSecret: true, Value: "testval"} 41 | 42 | var s rivertypes.Secret 43 | err := decodeTo(t, input, &s) 44 | require.NoError(t, err) 45 | require.Equal(t, rivertypes.Secret("testval"), s) 46 | }) 47 | 48 | t.Run("conversion from string is allowed", func(t *testing.T) { 49 | var s rivertypes.OptionalSecret 50 | err := decodeTo(t, string("Hello, world!"), &s) 51 | require.NoError(t, err) 52 | 53 | expect := rivertypes.OptionalSecret{ 54 | IsSecret: false, 55 | Value: "Hello, world!", 56 | } 57 | require.Equal(t, expect, s) 58 | }) 59 | 60 | t.Run("conversion from secret is allowed", func(t *testing.T) { 61 | var s rivertypes.OptionalSecret 62 | err := decodeTo(t, rivertypes.Secret("Hello, world!"), &s) 63 | require.NoError(t, err) 64 | 65 | expect := rivertypes.OptionalSecret{ 66 | IsSecret: true, 67 | Value: "Hello, world!", 68 | } 69 | require.Equal(t, expect, s) 70 | }) 71 | } 72 | 73 | func TestOptionalSecret_Write(t *testing.T) { 74 | tt := []struct { 75 | name string 76 | value interface{} 77 | expect string 78 | }{ 79 | {"non-sensitive", rivertypes.OptionalSecret{Value: "foobar"}, `"foobar"`}, 80 | {"sensitive", rivertypes.OptionalSecret{IsSecret: true, Value: "foobar"}, `(secret)`}, 81 | {"non-sensitive pointer", &rivertypes.OptionalSecret{Value: "foobar"}, `"foobar"`}, 82 | {"sensitive pointer", &rivertypes.OptionalSecret{IsSecret: true, Value: "foobar"}, `(secret)`}, 83 | } 84 | 85 | for _, tc := range tt { 86 | t.Run(tc.name, func(t *testing.T) { 87 | be := builder.NewExpr() 88 | be.SetValue(tc.value) 89 | require.Equal(t, tc.expect, string(be.Bytes())) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rivertypes/secret.go: -------------------------------------------------------------------------------- 1 | package rivertypes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/river/internal/value" 7 | "github.com/grafana/river/token" 8 | "github.com/grafana/river/token/builder" 9 | ) 10 | 11 | // Secret is a River capsule holding a sensitive string. The contents of a 12 | // Secret are never displayed to the user when rendering River. 13 | // 14 | // Secret allows itself to be converted from a string River value, but never 15 | // the inverse. This ensures that a user can't accidentally leak a sensitive 16 | // value. 17 | type Secret string 18 | 19 | var ( 20 | _ value.Capsule = Secret("") 21 | _ value.ConvertibleIntoCapsule = Secret("") 22 | _ value.ConvertibleFromCapsule = (*Secret)(nil) 23 | 24 | _ builder.Tokenizer = Secret("") 25 | ) 26 | 27 | // RiverCapsule marks Secret as a RiverCapsule. 28 | func (s Secret) RiverCapsule() {} 29 | 30 | // ConvertInto converts the Secret and stores it into the Go value pointed at 31 | // by dst. Secrets can be converted into *OptionalSecret. In other cases, this 32 | // method will return an explicit error or river.ErrNoConversion. 33 | func (s Secret) ConvertInto(dst interface{}) error { 34 | switch dst := dst.(type) { 35 | case *OptionalSecret: 36 | *dst = OptionalSecret{IsSecret: true, Value: string(s)} 37 | return nil 38 | case *string: 39 | return fmt.Errorf("secrets may not be converted into strings") 40 | } 41 | 42 | return value.ErrNoConversion 43 | } 44 | 45 | // ConvertFrom converts the src value and stores it into the Secret s. 46 | // OptionalSecrets and strings can be converted into a Secret. In other cases, 47 | // this method will return river.ErrNoConversion. 48 | func (s *Secret) ConvertFrom(src interface{}) error { 49 | switch src := src.(type) { 50 | case OptionalSecret: 51 | *s = Secret(src.Value) 52 | return nil 53 | case string: 54 | *s = Secret(src) 55 | return nil 56 | } 57 | 58 | return value.ErrNoConversion 59 | } 60 | 61 | // RiverTokenize returns a set of custom tokens to represent this value in 62 | // River text. 63 | func (s Secret) RiverTokenize() []builder.Token { 64 | return []builder.Token{{Tok: token.LITERAL, Lit: "(secret)"}} 65 | } 66 | -------------------------------------------------------------------------------- /rivertypes/secret_test.go: -------------------------------------------------------------------------------- 1 | package rivertypes_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/river/parser" 7 | "github.com/grafana/river/rivertypes" 8 | "github.com/grafana/river/vm" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestSecret(t *testing.T) { 13 | t.Run("strings can be converted to secret", func(t *testing.T) { 14 | var s rivertypes.Secret 15 | err := decodeTo(t, string("Hello, world!"), &s) 16 | require.NoError(t, err) 17 | require.Equal(t, rivertypes.Secret("Hello, world!"), s) 18 | }) 19 | 20 | t.Run("secrets cannot be converted to strings", func(t *testing.T) { 21 | var s string 22 | err := decodeTo(t, rivertypes.Secret("Hello, world!"), &s) 23 | require.NotNil(t, err) 24 | require.Contains(t, err.Error(), "secrets may not be converted into strings") 25 | }) 26 | 27 | t.Run("secrets can be passed to secrets", func(t *testing.T) { 28 | var s rivertypes.Secret 29 | err := decodeTo(t, rivertypes.Secret("Hello, world!"), &s) 30 | require.NoError(t, err) 31 | require.Equal(t, rivertypes.Secret("Hello, world!"), s) 32 | }) 33 | } 34 | 35 | func decodeTo(t *testing.T, input interface{}, target interface{}) error { 36 | t.Helper() 37 | 38 | expr, err := parser.ParseExpression("val") 39 | require.NoError(t, err) 40 | 41 | eval := vm.New(expr) 42 | return eval.Evaluate(&vm.Scope{ 43 | Variables: map[string]interface{}{ 44 | "val": input, 45 | }, 46 | }, target) 47 | } 48 | -------------------------------------------------------------------------------- /scanner/identifier.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/river/token" 7 | ) 8 | 9 | // IsValidIdentifier returns true if the given string is a valid river 10 | // identifier. 11 | func IsValidIdentifier(in string) bool { 12 | s := New(token.NewFile(""), []byte(in), nil, 0) 13 | _, tok, lit := s.Scan() 14 | return tok == token.IDENT && lit == in 15 | } 16 | 17 | // SanitizeIdentifier will return the given string mutated into a valid river 18 | // identifier. If the given string is already a valid identifier, it will be 19 | // returned unchanged. 20 | // 21 | // This should be used with caution since the different inputs can result in 22 | // identical outputs. 23 | func SanitizeIdentifier(in string) (string, error) { 24 | if in == "" { 25 | return "", fmt.Errorf("cannot generate a new identifier for an empty string") 26 | } 27 | 28 | if IsValidIdentifier(in) { 29 | return in, nil 30 | } 31 | 32 | newValue := generateNewIdentifier(in) 33 | if !IsValidIdentifier(newValue) { 34 | panic(fmt.Errorf("invalid identifier %q generated for `%q`", newValue, in)) 35 | } 36 | 37 | return newValue, nil 38 | } 39 | 40 | // generateNewIdentifier expects a valid river prefix and replacement 41 | // string and returns a new identifier based on the given input. 42 | func generateNewIdentifier(in string) string { 43 | newValue := "" 44 | for i, c := range in { 45 | if i == 0 { 46 | if isDigit(c) { 47 | newValue = "_" 48 | } 49 | } 50 | 51 | if !(isLetter(c) || isDigit(c)) { 52 | newValue += "_" 53 | continue 54 | } 55 | 56 | newValue += string(c) 57 | } 58 | 59 | return newValue 60 | } 61 | -------------------------------------------------------------------------------- /scanner/identifier_test.go: -------------------------------------------------------------------------------- 1 | package scanner_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/river/scanner" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var validTestCases = []struct { 11 | name string 12 | identifier string 13 | expect bool 14 | }{ 15 | {"empty", "", false}, 16 | {"start_number", "0identifier_1", false}, 17 | {"start_char", "identifier_1", true}, 18 | {"start_underscore", "_identifier_1", true}, 19 | {"special_chars", "!@#$%^&*()", false}, 20 | {"special_char", "identifier_1!", false}, 21 | {"spaces", "identifier _ 1", false}, 22 | } 23 | 24 | func TestIsValidIdentifier(t *testing.T) { 25 | for _, tc := range validTestCases { 26 | t.Run(tc.name, func(t *testing.T) { 27 | require.Equal(t, tc.expect, scanner.IsValidIdentifier(tc.identifier)) 28 | }) 29 | } 30 | } 31 | 32 | func BenchmarkIsValidIdentifier(b *testing.B) { 33 | for i := 0; i < b.N; i++ { 34 | for _, tc := range validTestCases { 35 | _ = scanner.IsValidIdentifier(tc.identifier) 36 | } 37 | } 38 | } 39 | 40 | var sanitizeTestCases = []struct { 41 | name string 42 | identifier string 43 | expectIdentifier string 44 | expectErr string 45 | }{ 46 | {"empty", "", "", "cannot generate a new identifier for an empty string"}, 47 | {"start_number", "0identifier_1", "_0identifier_1", ""}, 48 | {"start_char", "identifier_1", "identifier_1", ""}, 49 | {"start_underscore", "_identifier_1", "_identifier_1", ""}, 50 | {"special_chars", "!@#$%^&*()", "__________", ""}, 51 | {"special_char", "identifier_1!", "identifier_1_", ""}, 52 | {"spaces", "identifier _ 1", "identifier___1", ""}, 53 | } 54 | 55 | func TestSanitizeIdentifier(t *testing.T) { 56 | for _, tc := range sanitizeTestCases { 57 | t.Run(tc.name, func(t *testing.T) { 58 | newIdentifier, err := scanner.SanitizeIdentifier(tc.identifier) 59 | if tc.expectErr != "" { 60 | require.EqualError(t, err, tc.expectErr) 61 | return 62 | } 63 | 64 | require.NoError(t, err) 65 | require.Equal(t, tc.expectIdentifier, newIdentifier) 66 | }) 67 | } 68 | } 69 | 70 | func BenchmarkSanitizeIdentifier(b *testing.B) { 71 | for i := 0; i < b.N; i++ { 72 | for _, tc := range sanitizeTestCases { 73 | _, _ = scanner.SanitizeIdentifier(tc.identifier) 74 | } 75 | } 76 | } 77 | 78 | func FuzzSanitizeIdentifier(f *testing.F) { 79 | for _, tc := range sanitizeTestCases { 80 | f.Add(tc.identifier) 81 | } 82 | 83 | f.Fuzz(func(t *testing.T, input string) { 84 | newIdentifier, err := scanner.SanitizeIdentifier(input) 85 | if input == "" { 86 | require.EqualError(t, err, "cannot generate a new identifier for an empty string") 87 | return 88 | } 89 | require.NoError(t, err) 90 | require.True(t, scanner.IsValidIdentifier(newIdentifier)) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /scanner/scanner_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/grafana/river/token" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type tokenExample struct { 12 | tok token.Token 13 | lit string 14 | } 15 | 16 | var tokens = []tokenExample{ 17 | // Special tokens 18 | {token.COMMENT, "/* a comment */"}, 19 | {token.COMMENT, "// a comment \n"}, 20 | {token.COMMENT, "/*\r*/"}, 21 | {token.COMMENT, "/**\r/*/"}, // golang/go#11151 22 | {token.COMMENT, "/**\r\r/*/"}, 23 | {token.COMMENT, "//\r\n"}, 24 | 25 | // Identifiers and basic type literals 26 | {token.IDENT, "foobar"}, 27 | {token.IDENT, "a۰۱۸"}, 28 | {token.IDENT, "foo६४"}, 29 | {token.IDENT, "bar9876"}, 30 | {token.IDENT, "ŝ"}, // golang/go#4000 31 | {token.IDENT, "ŝfoo"}, // golang/go#4000 32 | {token.NUMBER, "0"}, 33 | {token.NUMBER, "1"}, 34 | {token.NUMBER, "123456789012345678890"}, 35 | {token.NUMBER, "01234567"}, 36 | {token.FLOAT, "0."}, 37 | {token.FLOAT, ".0"}, 38 | {token.FLOAT, "3.14159265"}, 39 | {token.FLOAT, "1e0"}, 40 | {token.FLOAT, "1e+100"}, 41 | {token.FLOAT, "1e-100"}, 42 | {token.FLOAT, "2.71828e-1000"}, 43 | {token.STRING, `"Hello, world!"`}, 44 | {token.STRING, "`Hello, world!\\\\`"}, 45 | 46 | // Operators and delimiters 47 | {token.ADD, "+"}, 48 | {token.SUB, "-"}, 49 | {token.MUL, "*"}, 50 | {token.DIV, "/"}, 51 | {token.MOD, "%"}, 52 | {token.POW, "^"}, 53 | 54 | {token.AND, "&&"}, 55 | {token.OR, "||"}, 56 | 57 | {token.EQ, "=="}, 58 | {token.LT, "<"}, 59 | {token.GT, ">"}, 60 | {token.ASSIGN, "="}, 61 | {token.NOT, "!"}, 62 | 63 | {token.NEQ, "!="}, 64 | {token.LTE, "<="}, 65 | {token.GTE, ">="}, 66 | 67 | {token.LPAREN, "("}, 68 | {token.LBRACK, "["}, 69 | {token.LCURLY, "{"}, 70 | {token.COMMA, ","}, 71 | {token.DOT, "."}, 72 | 73 | {token.RPAREN, ")"}, 74 | {token.RBRACK, "]"}, 75 | {token.RCURLY, "}"}, 76 | 77 | // Keywords 78 | {token.NULL, "null"}, 79 | {token.BOOL, "true"}, 80 | {token.BOOL, "false"}, 81 | } 82 | 83 | const whitespace = " \t \n\n\n" // Various whitespace to separate tokens 84 | 85 | var source = func() []byte { 86 | var src []byte 87 | for _, t := range tokens { 88 | src = append(src, t.lit...) 89 | src = append(src, whitespace...) 90 | } 91 | return src 92 | }() 93 | 94 | // FuzzScanner ensures that the scanner will always be able to reach EOF 95 | // regardless of input. 96 | func FuzzScanner(f *testing.F) { 97 | // Add each token into the corpus 98 | for _, t := range tokens { 99 | f.Add([]byte(t.lit)) 100 | } 101 | // Then add the entire source 102 | f.Add(source) 103 | 104 | f.Fuzz(func(t *testing.T, input []byte) { 105 | f := token.NewFile(t.Name()) 106 | 107 | s := New(f, input, nil, IncludeComments) 108 | 109 | for { 110 | _, tok, _ := s.Scan() 111 | if tok == token.EOF { 112 | break 113 | } 114 | } 115 | }) 116 | } 117 | 118 | func TestScanner_Scan(t *testing.T) { 119 | whitespaceLinecount := newlineCount(whitespace) 120 | 121 | var eh ErrorHandler = func(_ token.Pos, msg string) { 122 | t.Errorf("ErrorHandler called (msg = %s)", msg) 123 | } 124 | 125 | f := token.NewFile(t.Name()) 126 | s := New(f, source, eh, IncludeComments|dontInsertTerms) 127 | 128 | // Configure expected position 129 | expectPos := token.Position{ 130 | Filename: t.Name(), 131 | Offset: 0, 132 | Line: 1, 133 | Column: 1, 134 | } 135 | 136 | index := 0 137 | for { 138 | pos, tok, lit := s.Scan() 139 | 140 | // Check position 141 | checkPos(t, lit, tok, pos, expectPos) 142 | 143 | // Check token 144 | e := tokenExample{token.EOF, ""} 145 | if index < len(tokens) { 146 | e = tokens[index] 147 | index++ 148 | } 149 | assert.Equal(t, e.tok, tok) 150 | 151 | // Check literal 152 | expectLit := "" 153 | switch e.tok { 154 | case token.COMMENT: 155 | // no CRs in comments 156 | expectLit = string(stripCR([]byte(e.lit), e.lit[1] == '*')) 157 | if expectLit[1] == '/' { 158 | // Line comment literals doesn't contain newline 159 | expectLit = expectLit[0 : len(expectLit)-1] 160 | } 161 | case token.IDENT: 162 | expectLit = e.lit 163 | case token.NUMBER, token.FLOAT, token.STRING, token.NULL, token.BOOL: 164 | expectLit = e.lit 165 | } 166 | assert.Equal(t, expectLit, lit) 167 | 168 | if tok == token.EOF { 169 | break 170 | } 171 | 172 | // Update position 173 | expectPos.Offset += len(e.lit) + len(whitespace) 174 | expectPos.Line += newlineCount(e.lit) + whitespaceLinecount 175 | } 176 | 177 | if s.NumErrors() != 0 { 178 | assert.Zero(t, s.NumErrors(), "expected number of scanning errors to be 0") 179 | } 180 | } 181 | 182 | func newlineCount(s string) int { 183 | var n int 184 | for i := 0; i < len(s); i++ { 185 | if s[i] == '\n' { 186 | n++ 187 | } 188 | } 189 | return n 190 | } 191 | 192 | func checkPos(t *testing.T, lit string, tok token.Token, p token.Pos, expected token.Position) { 193 | t.Helper() 194 | 195 | pos := p.Position() 196 | 197 | // Check cleaned filenames so that we don't have to worry about different 198 | // os.PathSeparator values. 199 | if pos.Filename != expected.Filename && filepath.Clean(pos.Filename) != filepath.Clean(expected.Filename) { 200 | assert.Equal(t, expected.Filename, pos.Filename, "Bad filename for %s (%q)", tok, lit) 201 | } 202 | 203 | assert.Equal(t, expected.Offset, pos.Offset, "Bad offset for %s (%q)", tok, lit) 204 | assert.Equal(t, expected.Line, pos.Line, "Bad line for %s (%q)", tok, lit) 205 | assert.Equal(t, expected.Column, pos.Column, "Bad column for %s (%q)", tok, lit) 206 | } 207 | 208 | var errorTests = []struct { 209 | input string 210 | tok token.Token 211 | pos int 212 | lit string 213 | err string 214 | }{ 215 | {"\a", token.ILLEGAL, 0, "", "illegal character U+0007"}, 216 | {`…`, token.ILLEGAL, 0, "", "illegal character U+2026 '…'"}, 217 | {"..", token.DOT, 0, "", ""}, // two periods, not invalid token (golang/go#28112) 218 | {`'illegal string'`, token.ILLEGAL, 0, "", "illegal single-quoted string; use double quotes"}, 219 | {`""`, token.STRING, 0, `""`, ""}, 220 | {`"abc`, token.STRING, 0, `"abc`, "string literal not terminated"}, 221 | {"\"abc\n", token.STRING, 0, `"abc`, "string literal not terminated"}, 222 | {"\"abc\n ", token.STRING, 0, `"abc`, "string literal not terminated"}, 223 | {"\"abc\x00def\"", token.STRING, 4, "\"abc\x00def\"", "illegal character NUL"}, 224 | {"\"abc\x80def\"", token.STRING, 4, "\"abc\x80def\"", "illegal UTF-8 encoding"}, 225 | {"\ufeff\ufeff", token.ILLEGAL, 3, "\ufeff\ufeff", "illegal byte order mark"}, // only first BOM is ignored 226 | {"//\ufeff", token.COMMENT, 2, "//\ufeff", "illegal byte order mark"}, // only first BOM is ignored 227 | {`"` + "abc\ufeffdef" + `"`, token.STRING, 4, `"` + "abc\ufeffdef" + `"`, "illegal byte order mark"}, // only first BOM is ignored 228 | {"abc\x00def", token.IDENT, 3, "abc", "illegal character NUL"}, 229 | {"abc\x00", token.IDENT, 3, "abc", "illegal character NUL"}, 230 | {"10E", token.FLOAT, 0, "10E", "exponent has no digits"}, 231 | } 232 | 233 | func TestScanner_Scan_Errors(t *testing.T) { 234 | for _, e := range errorTests { 235 | checkError(t, e.input, e.tok, e.pos, e.lit, e.err) 236 | } 237 | } 238 | 239 | func checkError(t *testing.T, src string, tok token.Token, pos int, lit, err string) { 240 | t.Helper() 241 | 242 | var ( 243 | actualErrors int 244 | latestError string 245 | latestPos token.Pos 246 | ) 247 | 248 | eh := func(pos token.Pos, msg string) { 249 | actualErrors++ 250 | latestError = msg 251 | latestPos = pos 252 | } 253 | 254 | f := token.NewFile(t.Name()) 255 | s := New(f, []byte(src), eh, IncludeComments|dontInsertTerms) 256 | 257 | _, actualTok, actualLit := s.Scan() 258 | 259 | assert.Equal(t, tok, actualTok) 260 | if actualTok != token.ILLEGAL { 261 | assert.Equal(t, lit, actualLit) 262 | } 263 | 264 | expectErrors := 0 265 | if err != "" { 266 | expectErrors = 1 267 | } 268 | 269 | assert.Equal(t, expectErrors, actualErrors, "Unexpected error count in src %q", src) 270 | assert.Equal(t, err, latestError, "Unexpected error message in src %q", src) 271 | assert.Equal(t, pos, latestPos.Offset(), "Unexpected offset in src %q", src) 272 | } 273 | -------------------------------------------------------------------------------- /token/builder/nested_defaults_test.go: -------------------------------------------------------------------------------- 1 | package builder_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/grafana/river/ast" 9 | "github.com/grafana/river/parser" 10 | "github.com/grafana/river/token/builder" 11 | "github.com/grafana/river/vm" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const ( 16 | defaultNumber = 123 17 | otherDefaultNumber = 321 18 | ) 19 | 20 | var testCases = []struct { 21 | name string 22 | input interface{} 23 | expectedRiver string 24 | }{ 25 | { 26 | name: "struct propagating default - input matching default", 27 | input: StructPropagatingDefault{Inner: AttrWithDefault{Number: defaultNumber}}, 28 | expectedRiver: "", 29 | }, 30 | { 31 | name: "struct propagating default - input with zero-value struct", 32 | input: StructPropagatingDefault{}, 33 | expectedRiver: ` 34 | inner { 35 | number = 0 36 | } 37 | `, 38 | }, 39 | { 40 | name: "struct propagating default - input with non-default value", 41 | input: StructPropagatingDefault{Inner: AttrWithDefault{Number: 42}}, 42 | expectedRiver: ` 43 | inner { 44 | number = 42 45 | } 46 | `, 47 | }, 48 | { 49 | name: "pointer propagating default - input matching default", 50 | input: PtrPropagatingDefault{Inner: &AttrWithDefault{Number: defaultNumber}}, 51 | expectedRiver: "", 52 | }, 53 | { 54 | name: "pointer propagating default - input with zero value", 55 | input: PtrPropagatingDefault{Inner: &AttrWithDefault{}}, 56 | expectedRiver: ` 57 | inner { 58 | number = 0 59 | } 60 | `, 61 | }, 62 | { 63 | name: "pointer propagating default - input with non-default value", 64 | input: PtrPropagatingDefault{Inner: &AttrWithDefault{Number: 42}}, 65 | expectedRiver: ` 66 | inner { 67 | number = 42 68 | } 69 | `, 70 | }, 71 | { 72 | name: "zero default - input with zero value", 73 | input: ZeroDefault{Inner: &AttrWithDefault{}}, 74 | expectedRiver: "", 75 | }, 76 | { 77 | name: "zero default - input with non-default value", 78 | input: ZeroDefault{Inner: &AttrWithDefault{Number: 42}}, 79 | expectedRiver: ` 80 | inner { 81 | number = 42 82 | } 83 | `, 84 | }, 85 | { 86 | name: "no default - input with zero value", 87 | input: NoDefaultDefined{Inner: &AttrWithDefault{}}, 88 | expectedRiver: ` 89 | inner { 90 | number = 0 91 | } 92 | `, 93 | }, 94 | { 95 | name: "no default - input with non-default value", 96 | input: NoDefaultDefined{Inner: &AttrWithDefault{Number: 42}}, 97 | expectedRiver: ` 98 | inner { 99 | number = 42 100 | } 101 | `, 102 | }, 103 | { 104 | name: "mismatching default - input matching outer default", 105 | input: MismatchingDefault{Inner: &AttrWithDefault{Number: otherDefaultNumber}}, 106 | expectedRiver: "", 107 | }, 108 | { 109 | name: "mismatching default - input matching inner default", 110 | input: MismatchingDefault{Inner: &AttrWithDefault{Number: defaultNumber}}, 111 | expectedRiver: "inner { }", 112 | }, 113 | { 114 | name: "mismatching default - input with non-default value", 115 | input: MismatchingDefault{Inner: &AttrWithDefault{Number: 42}}, 116 | expectedRiver: ` 117 | inner { 118 | number = 42 119 | } 120 | `, 121 | }, 122 | } 123 | 124 | func TestNestedDefaults(t *testing.T) { 125 | for _, tc := range testCases { 126 | t.Run(fmt.Sprintf("%T/%s", tc.input, tc.name), func(t *testing.T) { 127 | f := builder.NewFile() 128 | f.Body().AppendFrom(tc.input) 129 | actualRiver := string(f.Bytes()) 130 | expected := format(t, tc.expectedRiver) 131 | require.Equal(t, expected, actualRiver, "generated river didn't match expected") 132 | 133 | // Now decode the River produced above and make sure it's the same as the input. 134 | eval := vm.New(parseBlock(t, actualRiver)) 135 | vPtr := reflect.New(reflect.TypeOf(tc.input)).Interface() 136 | require.NoError(t, eval.Evaluate(nil, vPtr), "river evaluation error") 137 | 138 | actualOut := reflect.ValueOf(vPtr).Elem().Interface() 139 | require.Equal(t, tc.input, actualOut, "Invariant violated: encoded and then decoded block didn't match the original value") 140 | }) 141 | } 142 | } 143 | 144 | func TestPtrPropagatingDefaultWithNil(t *testing.T) { 145 | // This is a special case - when defaults are correctly defined, the `Inner: nil` should mean to use defaults. 146 | // Encoding will encode to empty string and decoding will produce the default value - `Inner: {Number: 123}`. 147 | input := PtrPropagatingDefault{} 148 | expectedEncodedRiver := "" 149 | expectedDecoded := PtrPropagatingDefault{Inner: &AttrWithDefault{Number: 123}} 150 | 151 | f := builder.NewFile() 152 | f.Body().AppendFrom(input) 153 | actualRiver := string(f.Bytes()) 154 | expected := format(t, expectedEncodedRiver) 155 | require.Equal(t, expected, actualRiver, "generated river didn't match expected") 156 | 157 | // Now decode the River produced above and make sure it's the same as the input. 158 | eval := vm.New(parseBlock(t, actualRiver)) 159 | vPtr := reflect.New(reflect.TypeOf(input)).Interface() 160 | require.NoError(t, eval.Evaluate(nil, vPtr), "river evaluation error") 161 | 162 | actualOut := reflect.ValueOf(vPtr).Elem().Interface() 163 | require.Equal(t, expectedDecoded, actualOut) 164 | } 165 | 166 | // StructPropagatingDefault has the outer defaults matching the inner block's defaults. The inner block is a struct. 167 | type StructPropagatingDefault struct { 168 | Inner AttrWithDefault `river:"inner,block,optional"` 169 | } 170 | 171 | func (o *StructPropagatingDefault) SetToDefault() { 172 | inner := &AttrWithDefault{} 173 | inner.SetToDefault() 174 | *o = StructPropagatingDefault{Inner: *inner} 175 | } 176 | 177 | // PtrPropagatingDefault has the outer defaults matching the inner block's defaults. The inner block is a pointer. 178 | type PtrPropagatingDefault struct { 179 | Inner *AttrWithDefault `river:"inner,block,optional"` 180 | } 181 | 182 | func (o *PtrPropagatingDefault) SetToDefault() { 183 | inner := &AttrWithDefault{} 184 | inner.SetToDefault() 185 | *o = PtrPropagatingDefault{Inner: inner} 186 | } 187 | 188 | // MismatchingDefault has the outer defaults NOT matching the inner block's defaults. The inner block is a pointer. 189 | type MismatchingDefault struct { 190 | Inner *AttrWithDefault `river:"inner,block,optional"` 191 | } 192 | 193 | func (o *MismatchingDefault) SetToDefault() { 194 | *o = MismatchingDefault{Inner: &AttrWithDefault{ 195 | Number: otherDefaultNumber, 196 | }} 197 | } 198 | 199 | // ZeroDefault has the outer defaults setting to zero values. The inner block is a pointer. 200 | type ZeroDefault struct { 201 | Inner *AttrWithDefault `river:"inner,block,optional"` 202 | } 203 | 204 | func (o *ZeroDefault) SetToDefault() { 205 | *o = ZeroDefault{Inner: &AttrWithDefault{}} 206 | } 207 | 208 | // NoDefaultDefined has no defaults defined. The inner block is a pointer. 209 | type NoDefaultDefined struct { 210 | Inner *AttrWithDefault `river:"inner,block,optional"` 211 | } 212 | 213 | // AttrWithDefault has a default value of a non-zero number. 214 | type AttrWithDefault struct { 215 | Number int `river:"number,attr,optional"` 216 | } 217 | 218 | func (i *AttrWithDefault) SetToDefault() { 219 | *i = AttrWithDefault{Number: defaultNumber} 220 | } 221 | 222 | func parseBlock(t *testing.T, input string) *ast.BlockStmt { 223 | t.Helper() 224 | 225 | input = fmt.Sprintf("test { %s }", input) 226 | res, err := parser.ParseFile("", []byte(input)) 227 | require.NoError(t, err) 228 | require.Len(t, res.Body, 1) 229 | 230 | stmt, ok := res.Body[0].(*ast.BlockStmt) 231 | require.True(t, ok, "Expected stmt to be a ast.BlockStmt, got %T", res.Body[0]) 232 | return stmt 233 | } 234 | -------------------------------------------------------------------------------- /token/builder/token.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/grafana/river/parser" 8 | "github.com/grafana/river/printer" 9 | "github.com/grafana/river/token" 10 | ) 11 | 12 | // A Token is a wrapper around token.Token which contains the token type 13 | // alongside its literal. Use LiteralTok as the Tok field to write literal 14 | // characters such as whitespace. 15 | type Token struct { 16 | Tok token.Token 17 | Lit string 18 | } 19 | 20 | // printFileTokens prints out the tokens as River text and formats them, writing 21 | // the final result to w. 22 | func printFileTokens(w io.Writer, toks []Token) (int, error) { 23 | var raw bytes.Buffer 24 | for _, tok := range toks { 25 | switch { 26 | case tok.Tok == token.LITERAL: 27 | raw.WriteString(tok.Lit) 28 | case tok.Tok == token.COMMENT: 29 | raw.WriteString(tok.Lit) 30 | case tok.Tok.IsLiteral() || tok.Tok.IsKeyword(): 31 | raw.WriteString(tok.Lit) 32 | default: 33 | raw.WriteString(tok.Tok.String()) 34 | } 35 | } 36 | 37 | f, err := parser.ParseFile("", raw.Bytes()) 38 | if err != nil { 39 | return 0, err 40 | } 41 | 42 | wc := &writerCount{w: w} 43 | err = printer.Fprint(wc, f) 44 | return wc.n, err 45 | } 46 | 47 | // printExprTokens prints out the tokens as River text and formats them, 48 | // writing the final result to w. 49 | func printExprTokens(w io.Writer, toks []Token) (int, error) { 50 | var raw bytes.Buffer 51 | for _, tok := range toks { 52 | switch { 53 | case tok.Tok == token.LITERAL: 54 | raw.WriteString(tok.Lit) 55 | case tok.Tok.IsLiteral() || tok.Tok.IsKeyword(): 56 | raw.WriteString(tok.Lit) 57 | default: 58 | raw.WriteString(tok.Tok.String()) 59 | } 60 | } 61 | 62 | expr, err := parser.ParseExpression(raw.String()) 63 | if err != nil { 64 | return 0, err 65 | } 66 | 67 | wc := &writerCount{w: w} 68 | err = printer.Fprint(wc, expr) 69 | return wc.n, err 70 | } 71 | 72 | type writerCount struct { 73 | w io.Writer 74 | n int 75 | } 76 | 77 | func (wc *writerCount) Write(p []byte) (n int, err error) { 78 | n, err = wc.w.Write(p) 79 | wc.n += n 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /token/builder/value_tokens.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/grafana/river/internal/value" 8 | "github.com/grafana/river/scanner" 9 | "github.com/grafana/river/token" 10 | ) 11 | 12 | // TODO(rfratto): check for optional values 13 | 14 | // Tokenizer is any value which can return a raw set of tokens. 15 | type Tokenizer interface { 16 | // RiverTokenize returns the raw set of River tokens which are used when 17 | // printing out the value with river/token/builder. 18 | RiverTokenize() []Token 19 | } 20 | 21 | func tokenEncode(val interface{}) []Token { 22 | return valueTokens(value.Encode(val)) 23 | } 24 | 25 | func valueTokens(v value.Value) []Token { 26 | var toks []Token 27 | 28 | // If v is a Tokenizer, allow it to override what tokens get generated. 29 | if tk, ok := v.Interface().(Tokenizer); ok { 30 | return tk.RiverTokenize() 31 | } 32 | 33 | switch v.Type() { 34 | case value.TypeNull: 35 | toks = append(toks, Token{token.NULL, "null"}) 36 | 37 | case value.TypeNumber: 38 | toks = append(toks, Token{token.NUMBER, v.Number().ToString()}) 39 | 40 | case value.TypeString: 41 | toks = append(toks, Token{token.STRING, fmt.Sprintf("%q", v.Text())}) 42 | 43 | case value.TypeBool: 44 | toks = append(toks, Token{token.STRING, fmt.Sprintf("%v", v.Bool())}) 45 | 46 | case value.TypeArray: 47 | toks = append(toks, Token{token.LBRACK, ""}) 48 | elems := v.Len() 49 | for i := 0; i < elems; i++ { 50 | elem := v.Index(i) 51 | 52 | toks = append(toks, valueTokens(elem)...) 53 | if i+1 < elems { 54 | toks = append(toks, Token{token.COMMA, ""}) 55 | } 56 | } 57 | toks = append(toks, Token{token.RBRACK, ""}) 58 | 59 | case value.TypeObject: 60 | toks = append(toks, Token{token.LCURLY, ""}, Token{token.LITERAL, "\n"}) 61 | 62 | keys := v.Keys() 63 | 64 | // If v isn't an ordered object (i.e., a go map), sort the keys so they 65 | // have a deterministic print order. 66 | if !v.OrderedKeys() { 67 | sort.Strings(keys) 68 | } 69 | 70 | for i := 0; i < len(keys); i++ { 71 | if scanner.IsValidIdentifier(keys[i]) { 72 | toks = append(toks, Token{token.IDENT, keys[i]}) 73 | } else { 74 | toks = append(toks, Token{token.STRING, fmt.Sprintf("%q", keys[i])}) 75 | } 76 | 77 | field, _ := v.Key(keys[i]) 78 | toks = append(toks, Token{token.ASSIGN, ""}) 79 | toks = append(toks, valueTokens(field)...) 80 | toks = append(toks, Token{token.COMMA, ""}, Token{token.LITERAL, "\n"}) 81 | } 82 | toks = append(toks, Token{token.RCURLY, ""}) 83 | 84 | case value.TypeFunction: 85 | toks = append(toks, Token{token.LITERAL, v.Describe()}) 86 | 87 | case value.TypeCapsule: 88 | toks = append(toks, Token{token.LITERAL, v.Describe()}) 89 | 90 | default: 91 | panic(fmt.Sprintf("river/token/builder: unrecognized value type %q", v.Type())) 92 | } 93 | 94 | return toks 95 | } 96 | -------------------------------------------------------------------------------- /token/file.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | ) 8 | 9 | // NoPos is the zero value for Pos. It has no file or line information 10 | // associated with it, and NoPos.Valid is false. 11 | var NoPos = Pos{} 12 | 13 | // Pos is a compact representation of a position within a file. It can be 14 | // converted into a Position for a more convenient, but larger, representation. 15 | type Pos struct { 16 | file *File 17 | off int 18 | } 19 | 20 | // String returns the string form of the Pos (the offset). 21 | func (p Pos) String() string { return strconv.Itoa(p.off) } 22 | 23 | // File returns the file used by the Pos. This will be nil for invalid 24 | // positions. 25 | func (p Pos) File() *File { return p.file } 26 | 27 | // Position converts the Pos into a Position. 28 | func (p Pos) Position() Position { return p.file.PositionFor(p) } 29 | 30 | // Add creates a new Pos relative to p. 31 | func (p Pos) Add(n int) Pos { 32 | return Pos{ 33 | file: p.file, 34 | off: p.off + n, 35 | } 36 | } 37 | 38 | // Offset returns the byte offset associated with Pos. 39 | func (p Pos) Offset() int { return p.off } 40 | 41 | // Valid reports whether the Pos is valid. 42 | func (p Pos) Valid() bool { return p != NoPos } 43 | 44 | // Position holds full position information for a location within an individual 45 | // file. 46 | type Position struct { 47 | Filename string // Filename (if any) 48 | Offset int // Byte offset (starting at 0) 49 | Line int // Line number (starting at 1) 50 | Column int // Offset from start of line (starting at 1) 51 | } 52 | 53 | // Valid reports whether the position is valid. Valid positions must have a 54 | // Line value greater than 0. 55 | func (pos *Position) Valid() bool { return pos.Line > 0 } 56 | 57 | // String returns a string in one of the following forms: 58 | // 59 | // file:line:column Valid position with file name 60 | // file:line Valid position with file name but no column 61 | // line:column Valid position with no file name 62 | // line Valid position with no file name or column 63 | // file Invalid position with file name 64 | // - Invalid position with no file name 65 | func (pos Position) String() string { 66 | s := pos.Filename 67 | 68 | if pos.Valid() { 69 | if s != "" { 70 | s += ":" 71 | } 72 | s += fmt.Sprintf("%d", pos.Line) 73 | if pos.Column != 0 { 74 | s += fmt.Sprintf(":%d", pos.Column) 75 | } 76 | } 77 | 78 | if s == "" { 79 | s = "-" 80 | } 81 | return s 82 | } 83 | 84 | // File holds position information for a specific file. 85 | type File struct { 86 | filename string 87 | lines []int // Byte offset of each line number (first element is always 0). 88 | } 89 | 90 | // NewFile creates a new File for storing position information. 91 | func NewFile(filename string) *File { 92 | return &File{ 93 | filename: filename, 94 | lines: []int{0}, 95 | } 96 | } 97 | 98 | // Pos returns a Pos given a byte offset. Pos panics if off is < 0. 99 | func (f *File) Pos(off int) Pos { 100 | if off < 0 { 101 | panic("Pos: illegal offset") 102 | } 103 | return Pos{file: f, off: off} 104 | } 105 | 106 | // Name returns the name of the file. 107 | func (f *File) Name() string { return f.filename } 108 | 109 | // AddLine tracks a new line from a byte offset. The line offset must be larger 110 | // than the offset for the previous line, otherwise the line offset is ignored. 111 | func (f *File) AddLine(offset int) { 112 | lines := len(f.lines) 113 | if f.lines[lines-1] < offset { 114 | f.lines = append(f.lines, offset) 115 | } 116 | } 117 | 118 | // PositionFor returns a Position from an offset. 119 | func (f *File) PositionFor(p Pos) Position { 120 | if p == NoPos { 121 | return Position{} 122 | } 123 | 124 | // Search through our line offsets to find the line/column info. The else 125 | // case should never happen here, but if it does, Position.Valid will return 126 | // false. 127 | var line, column int 128 | if i := searchInts(f.lines, p.off); i >= 0 { 129 | line, column = i+1, p.off-f.lines[i]+1 130 | } 131 | 132 | return Position{ 133 | Filename: f.filename, 134 | Offset: p.off, 135 | Line: line, 136 | Column: column, 137 | } 138 | } 139 | 140 | func searchInts(a []int, x int) int { 141 | return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1 142 | } 143 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | // Package token defines the lexical elements of a River config and utilities 2 | // surrounding their position. 3 | package token 4 | 5 | // Token is an individual River lexical token. 6 | type Token int 7 | 8 | // List of all lexical tokens and examples that represent them. 9 | // 10 | // LITERAL is used by token/builder to represent literal strings for writing 11 | // tokens, but never used for reading (so scanner never returns a 12 | // token.LITERAL). 13 | const ( 14 | ILLEGAL Token = iota // Invalid token. 15 | LITERAL // Literal text. 16 | EOF // End-of-file. 17 | COMMENT // // Hello, world! 18 | 19 | literalBeg 20 | IDENT // foobar 21 | NUMBER // 1234 22 | FLOAT // 1234.0 23 | STRING // "foobar" 24 | literalEnd 25 | 26 | keywordBeg 27 | BOOL // true 28 | NULL // null 29 | keywordEnd 30 | 31 | operatorBeg 32 | OR // || 33 | AND // && 34 | NOT // ! 35 | 36 | ASSIGN // = 37 | 38 | EQ // == 39 | NEQ // != 40 | LT // < 41 | LTE // <= 42 | GT // > 43 | GTE // >= 44 | 45 | ADD // + 46 | SUB // - 47 | MUL // * 48 | DIV // / 49 | MOD // % 50 | POW // ^ 51 | 52 | LCURLY // { 53 | RCURLY // } 54 | LPAREN // ( 55 | RPAREN // ) 56 | LBRACK // [ 57 | RBRACK // ] 58 | COMMA // , 59 | DOT // . 60 | operatorEnd 61 | 62 | TERMINATOR // \n 63 | ) 64 | 65 | var tokenNames = [...]string{ 66 | ILLEGAL: "ILLEGAL", 67 | LITERAL: "LITERAL", 68 | EOF: "EOF", 69 | COMMENT: "COMMENT", 70 | 71 | IDENT: "IDENT", 72 | NUMBER: "NUMBER", 73 | FLOAT: "FLOAT", 74 | STRING: "STRING", 75 | BOOL: "BOOL", 76 | NULL: "NULL", 77 | 78 | OR: "||", 79 | AND: "&&", 80 | NOT: "!", 81 | 82 | ASSIGN: "=", 83 | EQ: "==", 84 | NEQ: "!=", 85 | LT: "<", 86 | LTE: "<=", 87 | GT: ">", 88 | GTE: ">=", 89 | 90 | ADD: "+", 91 | SUB: "-", 92 | MUL: "*", 93 | DIV: "/", 94 | MOD: "%", 95 | POW: "^", 96 | 97 | LCURLY: "{", 98 | RCURLY: "}", 99 | LPAREN: "(", 100 | RPAREN: ")", 101 | LBRACK: "[", 102 | RBRACK: "]", 103 | COMMA: ",", 104 | DOT: ".", 105 | 106 | TERMINATOR: "TERMINATOR", 107 | } 108 | 109 | // Lookup maps a string to its keyword token or IDENT if it's not a keyword. 110 | func Lookup(ident string) Token { 111 | switch ident { 112 | case "true", "false": 113 | return BOOL 114 | case "null": 115 | return NULL 116 | default: 117 | return IDENT 118 | } 119 | } 120 | 121 | // String returns the string representation corresponding to the token. 122 | func (t Token) String() string { 123 | if int(t) >= len(tokenNames) { 124 | return "ILLEGAL" 125 | } 126 | 127 | name := tokenNames[t] 128 | if name == "" { 129 | return "ILLEGAL" 130 | } 131 | return name 132 | } 133 | 134 | // GoString returns the %#v format of t. 135 | func (t Token) GoString() string { return t.String() } 136 | 137 | // IsKeyword returns true if the token corresponds to a keyword. 138 | func (t Token) IsKeyword() bool { return t > keywordBeg && t < keywordEnd } 139 | 140 | // IsLiteral returns true if the token corresponds to a literal token or 141 | // identifier. 142 | func (t Token) IsLiteral() bool { return t > literalBeg && t < literalEnd } 143 | 144 | // IsOperator returns true if the token corresponds to an operator or 145 | // delimiter. 146 | func (t Token) IsOperator() bool { return t > operatorBeg && t < operatorEnd } 147 | 148 | // BinaryPrecedence returns the operator precedence of the binary operator t. 149 | // If t is not a binary operator, the result is LowestPrecedence. 150 | func (t Token) BinaryPrecedence() int { 151 | switch t { 152 | case OR: 153 | return 1 154 | case AND: 155 | return 2 156 | case EQ, NEQ, LT, LTE, GT, GTE: 157 | return 3 158 | case ADD, SUB: 159 | return 4 160 | case MUL, DIV, MOD: 161 | return 5 162 | case POW: 163 | return 6 164 | } 165 | 166 | return LowestPrecedence 167 | } 168 | 169 | // Levels of precedence for operator tokens. 170 | const ( 171 | LowestPrecedence = 0 // non-operators 172 | UnaryPrecedence = 7 173 | HighestPrecedence = 8 174 | ) 175 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package river 2 | 3 | import "github.com/grafana/river/internal/value" 4 | 5 | // Our types in this file are re-implementations of interfaces from 6 | // value.Capsule. They are *not* defined as type aliases, since pkg.go.dev 7 | // would show the type alias instead of the contents of that type (which IMO is 8 | // a frustrating user experience). 9 | // 10 | // The types below must be kept in sync with the internal package, and the 11 | // checks below ensure they're compatible. 12 | var ( 13 | _ value.Defaulter = (Defaulter)(nil) 14 | _ value.Unmarshaler = (Unmarshaler)(nil) 15 | _ value.Validator = (Validator)(nil) 16 | _ value.Capsule = (Capsule)(nil) 17 | _ value.ConvertibleFromCapsule = (ConvertibleFromCapsule)(nil) 18 | _ value.ConvertibleIntoCapsule = (ConvertibleIntoCapsule)(nil) 19 | ) 20 | 21 | // The Unmarshaler interface allows a type to hook into River decoding and 22 | // decode into another type or provide pre-decoding logic. 23 | type Unmarshaler interface { 24 | // UnmarshalRiver is invoked when decoding a River value into a Go value. f 25 | // should be called with a pointer to a value to decode into. UnmarshalRiver 26 | // will not be called on types which are squashed into the parent struct 27 | // using `river:",squash"`. 28 | UnmarshalRiver(f func(v interface{}) error) error 29 | } 30 | 31 | // The Defaulter interface allows a type to implement default functionality 32 | // in River evaluation. 33 | type Defaulter interface { 34 | // SetToDefault is called when evaluating a block or body to set the value 35 | // to its defaults. SetToDefault will not be called on types which are 36 | // squashed into the parent struct using `river:",squash"`. 37 | SetToDefault() 38 | } 39 | 40 | // The Validator interface allows a type to implement validation functionality 41 | // in River evaluation. 42 | type Validator interface { 43 | // Validate is called when evaluating a block or body to enforce the 44 | // value is valid. Validate will not be called on types which are 45 | // squashed into the parent struct using `river:",squash"`. 46 | Validate() error 47 | } 48 | 49 | // Capsule is an interface marker which tells River that a type should always 50 | // be treated as a "capsule type" instead of the default type River would 51 | // assign. 52 | // 53 | // Capsule types are useful for passing around arbitrary Go values in River 54 | // expressions and for declaring new synthetic types with custom conversion 55 | // rules. 56 | // 57 | // By default, only two capsule values of the same underlying Go type are 58 | // compatible. Types which implement ConvertibleFromCapsule or 59 | // ConvertibleToCapsule can provide custom logic for conversions from and to 60 | // other types. 61 | type Capsule interface { 62 | // RiverCapsule marks the type as a Capsule. RiverCapsule is never invoked by 63 | // River. 64 | RiverCapsule() 65 | } 66 | 67 | // ErrNoConversion is returned by implementations of ConvertibleFromCapsule and 68 | // ConvertibleToCapsule when a conversion with a specific type is unavailable. 69 | // 70 | // Returning this error causes River to fall back to default conversion rules. 71 | var ErrNoConversion = value.ErrNoConversion 72 | 73 | // ConvertibleFromCapsule is a Capsule which supports custom conversion from 74 | // any Go type which is not the same as the capsule type. 75 | type ConvertibleFromCapsule interface { 76 | Capsule 77 | 78 | // ConvertFrom updates the ConvertibleFromCapsule value based on the value of 79 | // src. src may be any Go value, not just other capsules. 80 | // 81 | // ConvertFrom should return ErrNoConversion if no conversion is available 82 | // from src. Other errors are treated as a River decoding error. 83 | ConvertFrom(src interface{}) error 84 | } 85 | 86 | // ConvertibleIntoCapsule is a Capsule which supports custom conversion into 87 | // any Go type which is not the same as the capsule type. 88 | type ConvertibleIntoCapsule interface { 89 | Capsule 90 | 91 | // ConvertInto should convert its value and store it into dst. dst will be a 92 | // pointer to a Go value of any type. 93 | // 94 | // ConvertInto should return ErrNoConversion if no conversion into dst is 95 | // available. Other errors are treated as a River decoding error. 96 | ConvertInto(dst interface{}) error 97 | } 98 | -------------------------------------------------------------------------------- /vm/constant.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/grafana/river/internal/value" 8 | "github.com/grafana/river/token" 9 | ) 10 | 11 | func valueFromLiteral(lit string, tok token.Token) (value.Value, error) { 12 | // NOTE(rfratto): this function should never return an error, since the 13 | // parser only produces valid tokens; it can only fail if a user hand-builds 14 | // an AST with invalid literals. 15 | 16 | switch tok { 17 | case token.NULL: 18 | return value.Null, nil 19 | 20 | case token.NUMBER: 21 | intVal, err1 := strconv.ParseInt(lit, 0, 64) 22 | if err1 == nil { 23 | return value.Int(intVal), nil 24 | } 25 | 26 | uintVal, err2 := strconv.ParseUint(lit, 0, 64) 27 | if err2 == nil { 28 | return value.Uint(uintVal), nil 29 | } 30 | 31 | floatVal, err3 := strconv.ParseFloat(lit, 64) 32 | if err3 == nil { 33 | return value.Float(floatVal), nil 34 | } 35 | 36 | return value.Null, err3 37 | 38 | case token.FLOAT: 39 | v, err := strconv.ParseFloat(lit, 64) 40 | if err != nil { 41 | return value.Null, err 42 | } 43 | return value.Float(v), nil 44 | 45 | case token.STRING: 46 | v, err := strconv.Unquote(lit) 47 | if err != nil { 48 | return value.Null, err 49 | } 50 | return value.String(v), nil 51 | 52 | case token.BOOL: 53 | switch lit { 54 | case "true": 55 | return value.Bool(true), nil 56 | case "false": 57 | return value.Bool(false), nil 58 | default: 59 | return value.Null, fmt.Errorf("invalid boolean literal %q", lit) 60 | } 61 | default: 62 | panic(fmt.Sprintf("%v is not a valid token", tok)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /vm/error.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/grafana/river/ast" 8 | "github.com/grafana/river/diag" 9 | "github.com/grafana/river/internal/value" 10 | "github.com/grafana/river/printer" 11 | "github.com/grafana/river/token/builder" 12 | ) 13 | 14 | // makeDiagnostic tries to convert err into a diag.Diagnostic. err must be an 15 | // error from the river/internal/value package, otherwise err will be returned 16 | // unmodified. 17 | func makeDiagnostic(err error, assoc map[value.Value]ast.Node) error { 18 | var ( 19 | node ast.Node 20 | expr strings.Builder 21 | message string 22 | cause value.Value 23 | 24 | // Until we find a node, we're not a literal error. 25 | literal = false 26 | ) 27 | 28 | isValueError := value.WalkError(err, func(err error) { 29 | var val value.Value 30 | 31 | switch ne := err.(type) { 32 | case value.Error: 33 | message = ne.Error() 34 | val = ne.Value 35 | case value.TypeError: 36 | message = fmt.Sprintf("should be %s, got %s", ne.Expected, ne.Value.Type()) 37 | val = ne.Value 38 | case value.MissingKeyError: 39 | message = fmt.Sprintf("does not have field named %q", ne.Missing) 40 | val = ne.Value 41 | case value.ElementError: 42 | fmt.Fprintf(&expr, "[%d]", ne.Index) 43 | val = ne.Value 44 | case value.FieldError: 45 | fmt.Fprintf(&expr, ".%s", ne.Field) 46 | val = ne.Value 47 | } 48 | 49 | cause = val 50 | 51 | if foundNode, ok := assoc[val]; ok { 52 | // If we just found a direct node, we can reset the expression buffer so 53 | // we don't unnecessarily print element and field accesses for we can see 54 | // directly in the file. 55 | if literal { 56 | expr.Reset() 57 | } 58 | 59 | node = foundNode 60 | literal = true 61 | } else { 62 | literal = false 63 | } 64 | }) 65 | if !isValueError { 66 | return err 67 | } 68 | 69 | if node != nil { 70 | var nodeText strings.Builder 71 | if err := printer.Fprint(&nodeText, node); err != nil { 72 | // This should never panic; printer.Fprint only fails when given an 73 | // unexpected type, which we never do here. 74 | panic(err) 75 | } 76 | 77 | // Merge the node text with the expression together (which will be relative 78 | // accesses to the expression). 79 | message = fmt.Sprintf("%s%s %s", nodeText.String(), expr.String(), message) 80 | } else { 81 | message = fmt.Sprintf("%s %s", expr.String(), message) 82 | } 83 | 84 | // Render the underlying problematic value as a string. 85 | var valueText string 86 | if cause != value.Null { 87 | be := builder.NewExpr() 88 | be.SetValue(cause.Interface()) 89 | valueText = string(be.Bytes()) 90 | } 91 | if literal { 92 | // Hide the value if the node itself has the error we were worried about. 93 | valueText = "" 94 | } 95 | 96 | d := diag.Diagnostic{ 97 | Severity: diag.SeverityLevelError, 98 | Message: message, 99 | Value: valueText, 100 | } 101 | if node != nil { 102 | d.StartPos = ast.StartPos(node).Position() 103 | d.EndPos = ast.EndPos(node).Position() 104 | } 105 | return d 106 | } 107 | -------------------------------------------------------------------------------- /vm/op_binary_test.go: -------------------------------------------------------------------------------- 1 | package vm_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/grafana/river/parser" 8 | "github.com/grafana/river/rivertypes" 9 | "github.com/grafana/river/vm" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestVM_OptionalSecret_Conversion(t *testing.T) { 14 | scope := &vm.Scope{ 15 | Variables: map[string]any{ 16 | "string_val": "hello", 17 | "non_secret_val": rivertypes.OptionalSecret{IsSecret: false, Value: "world"}, 18 | "secret_val": rivertypes.OptionalSecret{IsSecret: true, Value: "secret"}, 19 | }, 20 | } 21 | 22 | tt := []struct { 23 | name string 24 | input string 25 | expect interface{} 26 | expectError string 27 | }{ 28 | { 29 | name: "string + capsule", 30 | input: `string_val + non_secret_val`, 31 | expect: string("helloworld"), 32 | }, 33 | { 34 | name: "capsule + string", 35 | input: `non_secret_val + string_val`, 36 | expect: string("worldhello"), 37 | }, 38 | { 39 | name: "string == capsule", 40 | input: `"world" == non_secret_val`, 41 | expect: bool(true), 42 | }, 43 | { 44 | name: "capsule == string", 45 | input: `non_secret_val == "world"`, 46 | expect: bool(true), 47 | }, 48 | { 49 | name: "capsule (secret) == capsule (secret)", 50 | input: `secret_val == secret_val`, 51 | expect: bool(true), 52 | }, 53 | { 54 | name: "capsule (non secret) == capsule (non secret)", 55 | input: `non_secret_val == non_secret_val`, 56 | expect: bool(true), 57 | }, 58 | { 59 | name: "capsule (non secret) == capsule (secret)", 60 | input: `non_secret_val == secret_val`, 61 | expect: bool(false), 62 | }, 63 | { 64 | name: "secret + string", 65 | input: `secret_val + string_val`, 66 | expectError: "secret_val should be one of [number string] for binop +", 67 | }, 68 | { 69 | name: "string + secret", 70 | input: `string_val + secret_val`, 71 | expectError: "secret_val should be one of [number string] for binop +", 72 | }, 73 | } 74 | 75 | for _, tc := range tt { 76 | t.Run(tc.name, func(t *testing.T) { 77 | expr, err := parser.ParseExpression(tc.input) 78 | require.NoError(t, err) 79 | 80 | expectTy := reflect.TypeOf(tc.expect) 81 | if expectTy == nil { 82 | expectTy = reflect.TypeOf((*any)(nil)).Elem() 83 | } 84 | rv := reflect.New(expectTy) 85 | 86 | if err := vm.New(expr).Evaluate(scope, rv.Interface()); tc.expectError == "" { 87 | require.NoError(t, err) 88 | require.Equal(t, tc.expect, rv.Elem().Interface()) 89 | } else { 90 | require.ErrorContains(t, err, tc.expectError) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /vm/op_unary.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "github.com/grafana/river/internal/value" 5 | "github.com/grafana/river/token" 6 | ) 7 | 8 | func evalUnaryOp(op token.Token, val value.Value) (value.Value, error) { 9 | switch op { 10 | case token.NOT: 11 | if val.Type() != value.TypeBool { 12 | return value.Null, value.TypeError{Value: val, Expected: value.TypeBool} 13 | } 14 | return value.Bool(!val.Bool()), nil 15 | 16 | case token.SUB: 17 | if val.Type() != value.TypeNumber { 18 | return value.Null, value.TypeError{Value: val, Expected: value.TypeNumber} 19 | } 20 | 21 | valNum := val.Number() 22 | switch valNum.Kind() { 23 | case value.NumberKindInt, value.NumberKindUint: 24 | // It doesn't make much sense to invert a uint, so we always cast to an 25 | // int and return an int. 26 | return value.Int(-valNum.Int()), nil 27 | case value.NumberKindFloat: 28 | return value.Float(-valNum.Float()), nil 29 | } 30 | } 31 | 32 | panic("river/vm: unreachable") 33 | } 34 | -------------------------------------------------------------------------------- /vm/tag_cache.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/grafana/river/internal/rivertags" 9 | ) 10 | 11 | // tagsCache caches the river tags for a struct type. This is never cleared, 12 | // but since most structs will be statically created throughout the lifetime 13 | // of the process, this will consume a negligible amount of memory. 14 | var tagsCache sync.Map 15 | 16 | func getCachedTagInfo(t reflect.Type) *tagInfo { 17 | if t.Kind() != reflect.Struct { 18 | panic("getCachedTagInfo called with non-struct type") 19 | } 20 | 21 | if entry, ok := tagsCache.Load(t); ok { 22 | return entry.(*tagInfo) 23 | } 24 | 25 | tfs := rivertags.Get(t) 26 | ti := &tagInfo{ 27 | Tags: tfs, 28 | TagLookup: make(map[string]rivertags.Field, len(tfs)), 29 | EnumLookup: make(map[string]enumBlock), // The length is not known ahead of time 30 | } 31 | 32 | for _, tf := range tfs { 33 | switch { 34 | case tf.IsAttr(), tf.IsBlock(): 35 | fullName := strings.Join(tf.Name, ".") 36 | ti.TagLookup[fullName] = tf 37 | 38 | case tf.IsEnum(): 39 | fullName := strings.Join(tf.Name, ".") 40 | 41 | // Find all the blocks that match to the enum, and inject them into the 42 | // EnumLookup table. 43 | enumFieldType := t.FieldByIndex(tf.Index).Type 44 | enumBlocksInfo := getCachedTagInfo(deferenceType(enumFieldType.Elem())) 45 | for _, blockField := range enumBlocksInfo.TagLookup { 46 | // The full name of the enum block is the name of the enum plus the 47 | // name of the block, separated by '.' 48 | enumBlockName := fullName + "." + strings.Join(blockField.Name, ".") 49 | ti.EnumLookup[enumBlockName] = enumBlock{ 50 | EnumField: tf, 51 | BlockField: blockField, 52 | } 53 | } 54 | } 55 | } 56 | 57 | tagsCache.Store(t, ti) 58 | return ti 59 | } 60 | 61 | func deferenceType(ty reflect.Type) reflect.Type { 62 | for ty.Kind() == reflect.Pointer { 63 | ty = ty.Elem() 64 | } 65 | return ty 66 | } 67 | 68 | type tagInfo struct { 69 | Tags []rivertags.Field 70 | TagLookup map[string]rivertags.Field 71 | 72 | // EnumLookup maps enum blocks to the enum field. For example, an enum block 73 | // called "foo.foo" and "foo.bar" will both map to the "foo" enum field. 74 | EnumLookup map[string]enumBlock 75 | } 76 | 77 | type enumBlock struct { 78 | EnumField rivertags.Field // Field in the parent struct of the enum slice 79 | BlockField rivertags.Field // Field in the enum struct for the enum block 80 | } 81 | -------------------------------------------------------------------------------- /vm/vm_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package vm_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/grafana/river/parser" 10 | "github.com/grafana/river/vm" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func BenchmarkExprs(b *testing.B) { 15 | // Shared scope across all tests below 16 | scope := &vm.Scope{ 17 | Variables: map[string]interface{}{ 18 | "foobar": int(42), 19 | }, 20 | } 21 | 22 | tt := []struct { 23 | name string 24 | input string 25 | expect interface{} 26 | }{ 27 | // Binops 28 | {"or", `false || true`, bool(true)}, 29 | {"and", `true && false`, bool(false)}, 30 | {"math/eq", `3 == 5`, bool(false)}, 31 | {"math/neq", `3 != 5`, bool(true)}, 32 | {"math/lt", `3 < 5`, bool(true)}, 33 | {"math/lte", `3 <= 5`, bool(true)}, 34 | {"math/gt", `3 > 5`, bool(false)}, 35 | {"math/gte", `3 >= 5`, bool(false)}, 36 | {"math/add", `3 + 5`, int(8)}, 37 | {"math/sub", `3 - 5`, int(-2)}, 38 | {"math/mul", `3 * 5`, int(15)}, 39 | {"math/div", `3 / 5`, int(0)}, 40 | {"math/mod", `5 % 3`, int(2)}, 41 | {"math/pow", `3 ^ 5`, int(243)}, 42 | {"binop chain", `3 + 5 * 2`, int(13)}, // Chain multiple binops 43 | 44 | // Identifier 45 | {"ident lookup", `foobar`, int(42)}, 46 | 47 | // Arrays 48 | {"array", `[0, 1, 2]`, []int{0, 1, 2}}, 49 | 50 | // Objects 51 | {"object to map", `{ a = 5, b = 10 }`, map[string]int{"a": 5, "b": 10}}, 52 | { 53 | name: "object to struct", 54 | input: `{ 55 | name = "John Doe", 56 | age = 42, 57 | }`, 58 | expect: struct { 59 | Name string `river:"name,attr"` 60 | Age int `river:"age,attr"` 61 | Country string `river:"country,attr,optional"` 62 | }{ 63 | Name: "John Doe", 64 | Age: 42, 65 | }, 66 | }, 67 | 68 | // Access 69 | {"access", `{ a = 15 }.a`, int(15)}, 70 | {"nested access", `{ a = { b = 12 } }.a.b`, int(12)}, 71 | 72 | // Indexing 73 | {"index", `[0, 1, 2][1]`, int(1)}, 74 | {"nested index", `[[1,2,3]][0][2]`, int(3)}, 75 | 76 | // Paren 77 | {"paren", `(15)`, int(15)}, 78 | 79 | // Unary 80 | {"unary not", `!true`, bool(false)}, 81 | {"unary neg", `-15`, int(-15)}, 82 | {"unary float", `-15.0`, float64(-15.0)}, 83 | {"unary int64", fmt.Sprintf("%v", math.MaxInt64), math.MaxInt64}, 84 | {"unary uint64", fmt.Sprintf("%v", uint64(math.MaxInt64)+1), uint64(math.MaxInt64) + 1}, 85 | // math.MaxUint64 + 1 = 18446744073709551616 86 | {"unary float64 from overflowing uint", "18446744073709551616", float64(18446744073709551616)}, 87 | } 88 | 89 | for _, tc := range tt { 90 | b.Run(tc.name, func(b *testing.B) { 91 | b.StopTimer() 92 | expr, err := parser.ParseExpression(tc.input) 93 | require.NoError(b, err) 94 | 95 | eval := vm.New(expr) 96 | b.StartTimer() 97 | 98 | expectType := reflect.TypeOf(tc.expect) 99 | 100 | for i := 0; i < b.N; i++ { 101 | vPtr := reflect.New(expectType).Interface() 102 | _ = eval.Evaluate(scope, vPtr) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /vm/vm_errors_test.go: -------------------------------------------------------------------------------- 1 | package vm_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/river/parser" 7 | "github.com/grafana/river/vm" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestVM_ExprErrors(t *testing.T) { 12 | type Target struct { 13 | Key struct { 14 | Object struct { 15 | Field1 []int `river:"field1,attr"` 16 | } `river:"object,attr"` 17 | } `river:"key,attr"` 18 | } 19 | 20 | tt := []struct { 21 | name string 22 | input string 23 | into interface{} 24 | scope *vm.Scope 25 | expect string 26 | }{ 27 | { 28 | name: "basic wrong type", 29 | input: `key = true`, 30 | into: &Target{}, 31 | expect: "test:1:7: true should be object, got bool", 32 | }, 33 | { 34 | name: "deeply nested literal", 35 | input: ` 36 | key = { 37 | object = { 38 | field1 = [15, 30, "Hello, world!"], 39 | }, 40 | } 41 | `, 42 | into: &Target{}, 43 | expect: `test:4:25: "Hello, world!" should be number, got string`, 44 | }, 45 | { 46 | name: "deeply nested indirect", 47 | input: `key = key_value`, 48 | into: &Target{}, 49 | scope: &vm.Scope{ 50 | Variables: map[string]interface{}{ 51 | "key_value": map[string]interface{}{ 52 | "object": map[string]interface{}{ 53 | "field1": []interface{}{15, 30, "Hello, world!"}, 54 | }, 55 | }, 56 | }, 57 | }, 58 | expect: `test:1:7: key_value.object.field1[2] should be number, got string`, 59 | }, 60 | { 61 | name: "complex expr", 62 | input: `key = [0, 1, 2]`, 63 | into: &struct { 64 | Key string `river:"key,attr"` 65 | }{}, 66 | expect: `test:1:7: [0, 1, 2] should be string, got array`, 67 | }, 68 | } 69 | 70 | for _, tc := range tt { 71 | t.Run(tc.name, func(t *testing.T) { 72 | res, err := parser.ParseFile("test", []byte(tc.input)) 73 | require.NoError(t, err) 74 | 75 | eval := vm.New(res) 76 | err = eval.Evaluate(tc.scope, tc.into) 77 | require.EqualError(t, err, tc.expect) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /vm/vm_stdlib_test.go: -------------------------------------------------------------------------------- 1 | package vm_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/grafana/river/internal/value" 9 | "github.com/grafana/river/parser" 10 | "github.com/grafana/river/rivertypes" 11 | "github.com/grafana/river/vm" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestVM_Stdlib(t *testing.T) { 16 | t.Setenv("TEST_VAR", "Hello!") 17 | 18 | tt := []struct { 19 | name string 20 | input string 21 | expect interface{} 22 | }{ 23 | {"env", `env("TEST_VAR")`, string("Hello!")}, 24 | {"concat", `concat([true, "foo"], [], [false, 1])`, []interface{}{true, "foo", false, 1}}, 25 | {"json_decode object", `json_decode("{\"foo\": \"bar\"}")`, map[string]interface{}{"foo": "bar"}}, 26 | {"json_decode array", `json_decode("[0, 1, 2]")`, []interface{}{float64(0), float64(1), float64(2)}}, 27 | {"json_decode nil field", `json_decode("{\"foo\": null}")`, map[string]interface{}{"foo": nil}}, 28 | {"json_decode nil array element", `json_decode("[0, null]")`, []interface{}{float64(0), nil}}, 29 | } 30 | 31 | for _, tc := range tt { 32 | t.Run(tc.name, func(t *testing.T) { 33 | expr, err := parser.ParseExpression(tc.input) 34 | require.NoError(t, err) 35 | 36 | eval := vm.New(expr) 37 | 38 | rv := reflect.New(reflect.TypeOf(tc.expect)) 39 | require.NoError(t, eval.Evaluate(nil, rv.Interface())) 40 | require.Equal(t, tc.expect, rv.Elem().Interface()) 41 | }) 42 | } 43 | } 44 | 45 | func TestStdlibCoalesce(t *testing.T) { 46 | t.Setenv("TEST_VAR2", "Hello!") 47 | 48 | tt := []struct { 49 | name string 50 | input string 51 | expect interface{} 52 | }{ 53 | {"coalesce()", `coalesce()`, value.Null}, 54 | {"coalesce(string)", `coalesce("Hello!")`, string("Hello!")}, 55 | {"coalesce(string, string)", `coalesce(env("TEST_VAR2"), "World!")`, string("Hello!")}, 56 | {"(string, string) with fallback", `coalesce(env("NON_DEFINED"), "World!")`, string("World!")}, 57 | {"coalesce(list, list)", `coalesce([], ["fallback"])`, []string{"fallback"}}, 58 | {"coalesce(list, list) with fallback", `coalesce(concat(["item"]), ["fallback"])`, []string{"item"}}, 59 | {"coalesce(int, int, int)", `coalesce(0, 1, 2)`, 1}, 60 | {"coalesce(bool, int, int)", `coalesce(false, 1, 2)`, 1}, 61 | {"coalesce(bool, bool)", `coalesce(false, true)`, true}, 62 | {"coalesce(list, bool)", `coalesce(json_decode("[]"), true)`, true}, 63 | {"coalesce(object, true) and return true", `coalesce(json_decode("{}"), true)`, true}, 64 | {"coalesce(object, false) and return false", `coalesce(json_decode("{}"), false)`, false}, 65 | {"coalesce(list, nil)", `coalesce([],null)`, value.Null}, 66 | } 67 | 68 | for _, tc := range tt { 69 | t.Run(tc.name, func(t *testing.T) { 70 | expr, err := parser.ParseExpression(tc.input) 71 | require.NoError(t, err) 72 | 73 | eval := vm.New(expr) 74 | 75 | rv := reflect.New(reflect.TypeOf(tc.expect)) 76 | require.NoError(t, eval.Evaluate(nil, rv.Interface())) 77 | require.Equal(t, tc.expect, rv.Elem().Interface()) 78 | }) 79 | } 80 | } 81 | 82 | func TestStdlibJsonPath(t *testing.T) { 83 | tt := []struct { 84 | name string 85 | input string 86 | expect interface{} 87 | }{ 88 | {"json_path with simple json", `json_path("{\"a\": \"b\"}", ".a")`, []string{"b"}}, 89 | {"json_path with simple json without results", `json_path("{\"a\": \"b\"}", ".nonexists")`, []string{}}, 90 | {"json_path with json array", `json_path("[{\"name\": \"Department\",\"value\": \"IT\"},{\"name\":\"ReferenceNumber\",\"value\":\"123456\"},{\"name\":\"TestStatus\",\"value\":\"Pending\"}]", "[?(@.name == \"Department\")].value")`, []string{"IT"}}, 91 | {"json_path with simple json and return first", `json_path("{\"a\": \"b\"}", ".a")[0]`, "b"}, 92 | } 93 | 94 | for _, tc := range tt { 95 | t.Run(tc.name, func(t *testing.T) { 96 | expr, err := parser.ParseExpression(tc.input) 97 | require.NoError(t, err) 98 | 99 | eval := vm.New(expr) 100 | 101 | rv := reflect.New(reflect.TypeOf(tc.expect)) 102 | require.NoError(t, eval.Evaluate(nil, rv.Interface())) 103 | require.Equal(t, tc.expect, rv.Elem().Interface()) 104 | }) 105 | } 106 | } 107 | 108 | func TestStdlib_Nonsensitive(t *testing.T) { 109 | scope := &vm.Scope{ 110 | Variables: map[string]any{ 111 | "secret": rivertypes.Secret("foo"), 112 | "optionalSecret": rivertypes.OptionalSecret{Value: "bar"}, 113 | }, 114 | } 115 | 116 | tt := []struct { 117 | name string 118 | input string 119 | expect interface{} 120 | }{ 121 | {"secret to string", `nonsensitive(secret)`, string("foo")}, 122 | {"optional secret to string", `nonsensitive(optionalSecret)`, string("bar")}, 123 | } 124 | 125 | for _, tc := range tt { 126 | t.Run(tc.name, func(t *testing.T) { 127 | expr, err := parser.ParseExpression(tc.input) 128 | require.NoError(t, err) 129 | 130 | eval := vm.New(expr) 131 | 132 | rv := reflect.New(reflect.TypeOf(tc.expect)) 133 | require.NoError(t, eval.Evaluate(scope, rv.Interface())) 134 | require.Equal(t, tc.expect, rv.Elem().Interface()) 135 | }) 136 | } 137 | } 138 | func TestStdlib_StringFunc(t *testing.T) { 139 | scope := &vm.Scope{ 140 | Variables: map[string]any{}, 141 | } 142 | 143 | tt := []struct { 144 | name string 145 | input string 146 | expect interface{} 147 | }{ 148 | {"to_lower", `to_lower("String")`, "string"}, 149 | {"to_upper", `to_upper("string")`, "STRING"}, 150 | {"trimspace", `trim_space(" string \n\n")`, "string"}, 151 | {"trimspace+to_upper+trim", `to_lower(to_upper(trim_space(" String ")))`, "string"}, 152 | {"split", `split("/aaa/bbb/ccc/ddd", "/")`, []string{"", "aaa", "bbb", "ccc", "ddd"}}, 153 | {"split+index", `split("/aaa/bbb/ccc/ddd", "/")[0]`, ""}, 154 | {"join+split", `join(split("/aaa/bbb/ccc/ddd", "/"), "/")`, "/aaa/bbb/ccc/ddd"}, 155 | {"join", `join(["foo", "bar", "baz"], ", ")`, "foo, bar, baz"}, 156 | {"join w/ int", `join([0, 0, 1], ", ")`, "0, 0, 1"}, 157 | {"format", `format("Hello %s", "World")`, "Hello World"}, 158 | {"format+int", `format("%#v", 1)`, "1"}, 159 | {"format+bool", `format("%#v", true)`, "true"}, 160 | {"format+quote", `format("%q", "hello")`, `"hello"`}, 161 | {"replace", `replace("Hello World", " World", "!")`, "Hello!"}, 162 | {"trim", `trim("?!hello?!", "!?")`, "hello"}, 163 | {"trim2", `trim(" hello! world.! ", "! ")`, "hello! world."}, 164 | {"trim_prefix", `trim_prefix("helloworld", "hello")`, "world"}, 165 | {"trim_suffix", `trim_suffix("helloworld", "world")`, "hello"}, 166 | } 167 | 168 | for _, tc := range tt { 169 | t.Run(tc.name, func(t *testing.T) { 170 | expr, err := parser.ParseExpression(tc.input) 171 | require.NoError(t, err) 172 | 173 | eval := vm.New(expr) 174 | 175 | rv := reflect.New(reflect.TypeOf(tc.expect)) 176 | require.NoError(t, eval.Evaluate(scope, rv.Interface())) 177 | require.Equal(t, tc.expect, rv.Elem().Interface()) 178 | }) 179 | } 180 | } 181 | 182 | func BenchmarkConcat(b *testing.B) { 183 | // There's a bit of setup work to do here: we want to create a scope holding 184 | // a slice of the Person type, which has a fair amount of data in it. 185 | // 186 | // We then want to pass it through concat. 187 | // 188 | // If the code path is fully optimized, there will be no intermediate 189 | // translations to interface{}. 190 | type Person struct { 191 | Name string `river:"name,attr"` 192 | Attrs map[string]string `river:"attrs,attr"` 193 | } 194 | type Body struct { 195 | Values []Person `river:"values,attr"` 196 | } 197 | 198 | in := `values = concat(values_ref)` 199 | f, err := parser.ParseFile("", []byte(in)) 200 | require.NoError(b, err) 201 | 202 | eval := vm.New(f) 203 | 204 | valuesRef := make([]Person, 0, 20) 205 | for i := 0; i < 20; i++ { 206 | data := make(map[string]string, 20) 207 | for j := 0; j < 20; j++ { 208 | var ( 209 | key = fmt.Sprintf("key_%d", i+1) 210 | value = fmt.Sprintf("value_%d", i+1) 211 | ) 212 | data[key] = value 213 | } 214 | valuesRef = append(valuesRef, Person{ 215 | Name: "Test Person", 216 | Attrs: data, 217 | }) 218 | } 219 | scope := &vm.Scope{ 220 | Variables: map[string]interface{}{ 221 | "values_ref": valuesRef, 222 | }, 223 | } 224 | 225 | // Reset timer before running the actual test 226 | b.ResetTimer() 227 | 228 | for i := 0; i < b.N; i++ { 229 | var b Body 230 | _ = eval.Evaluate(scope, &b) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /vm/vm_test.go: -------------------------------------------------------------------------------- 1 | package vm_test 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | "unicode" 8 | 9 | "github.com/grafana/river/parser" 10 | "github.com/grafana/river/scanner" 11 | "github.com/grafana/river/token" 12 | "github.com/grafana/river/vm" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestVM_Evaluate_Literals(t *testing.T) { 17 | tt := map[string]struct { 18 | input string 19 | expect interface{} 20 | }{ 21 | "number to int": {`12`, int(12)}, 22 | "number to int8": {`13`, int8(13)}, 23 | "number to int16": {`14`, int16(14)}, 24 | "number to int32": {`15`, int32(15)}, 25 | "number to int64": {`16`, int64(16)}, 26 | "number to uint": {`17`, uint(17)}, 27 | "number to uint8": {`18`, uint8(18)}, 28 | "number to uint16": {`19`, uint16(19)}, 29 | "number to uint32": {`20`, uint32(20)}, 30 | "number to uint64": {`21`, uint64(21)}, 31 | "number to float32": {`22`, float32(22)}, 32 | "number to float64": {`23`, float64(23)}, 33 | "number to string": {`24`, string("24")}, 34 | 35 | "float to float32": {`3.2`, float32(3.2)}, 36 | "float to float64": {`3.5`, float64(3.5)}, 37 | "float to string": {`3.9`, string("3.9")}, 38 | 39 | "float with dot to float32": {`.2`, float32(0.2)}, 40 | "float with dot to float64": {`.5`, float64(0.5)}, 41 | "float with dot to string": {`.9`, string("0.9")}, 42 | 43 | "string to string": {`"Hello, world!"`, string("Hello, world!")}, 44 | "string to int": {`"12"`, int(12)}, 45 | "string to float64": {`"12"`, float64(12)}, 46 | } 47 | 48 | for name, tc := range tt { 49 | t.Run(name, func(t *testing.T) { 50 | expr, err := parser.ParseExpression(tc.input) 51 | require.NoError(t, err) 52 | 53 | eval := vm.New(expr) 54 | 55 | vPtr := reflect.New(reflect.TypeOf(tc.expect)).Interface() 56 | require.NoError(t, eval.Evaluate(nil, vPtr)) 57 | 58 | actual := reflect.ValueOf(vPtr).Elem().Interface() 59 | require.Equal(t, tc.expect, actual) 60 | }) 61 | } 62 | } 63 | 64 | func TestVM_Evaluate(t *testing.T) { 65 | // Shared scope across all tests below 66 | scope := &vm.Scope{ 67 | Variables: map[string]interface{}{ 68 | "foobar": int(42), 69 | }, 70 | } 71 | 72 | tt := []struct { 73 | input string 74 | expect interface{} 75 | }{ 76 | // Binops 77 | {`true || false`, bool(true)}, 78 | {`false || false`, bool(false)}, 79 | {`true && false`, bool(false)}, 80 | {`true && true`, bool(true)}, 81 | {`3 == 5`, bool(false)}, 82 | {`3 == 3`, bool(true)}, 83 | {`3 != 5`, bool(true)}, 84 | {`3 < 5`, bool(true)}, 85 | {`3 <= 5`, bool(true)}, 86 | {`3 > 5`, bool(false)}, 87 | {`3 >= 5`, bool(false)}, 88 | {`3 + 5`, int(8)}, 89 | {`3 - 5`, int(-2)}, 90 | {`3 * 5`, int(15)}, 91 | {`3.0 / 5.0`, float64(0.6)}, 92 | {`5 % 3`, int(2)}, 93 | {`3 ^ 5`, int(243)}, 94 | {`3 + 5 * 2`, int(13)}, // Chain multiple binops 95 | {`42.0^-2`, float64(0.0005668934240362812)}, 96 | 97 | // Identifier 98 | {`foobar`, int(42)}, 99 | 100 | // Arrays 101 | {`[]`, []int{}}, 102 | {`[0, 1, 2]`, []int{0, 1, 2}}, 103 | {`[true, false]`, []bool{true, false}}, 104 | 105 | // Objects 106 | {`{ a = 5, b = 10 }`, map[string]int{"a": 5, "b": 10}}, 107 | { 108 | input: `{ 109 | name = "John Doe", 110 | age = 42, 111 | }`, 112 | expect: struct { 113 | Name string `river:"name,attr"` 114 | Age int `river:"age,attr"` 115 | Country string `river:"country,attr,optional"` 116 | }{ 117 | Name: "John Doe", 118 | Age: 42, 119 | }, 120 | }, 121 | 122 | // Access 123 | {`{ a = 15 }.a`, int(15)}, 124 | {`{ a = { b = 12 } }.a.b`, int(12)}, 125 | {`{}["foo"]`, nil}, 126 | 127 | // Indexing 128 | {`[0, 1, 2][1]`, int(1)}, 129 | {`[[1,2,3]][0][2]`, int(3)}, 130 | {`[true, false][0]`, bool(true)}, 131 | 132 | // Paren 133 | {`(15)`, int(15)}, 134 | 135 | // Unary 136 | {`!true`, bool(false)}, 137 | {`!false`, bool(true)}, 138 | {`-15`, int(-15)}, 139 | } 140 | 141 | for _, tc := range tt { 142 | name := trimWhitespace(tc.input) 143 | 144 | t.Run(name, func(t *testing.T) { 145 | expr, err := parser.ParseExpression(tc.input) 146 | require.NoError(t, err) 147 | 148 | eval := vm.New(expr) 149 | 150 | var vPtr any 151 | if tc.expect != nil { 152 | vPtr = reflect.New(reflect.TypeOf(tc.expect)).Interface() 153 | } else { 154 | // Create a new any pointer. 155 | vPtr = reflect.New(reflect.TypeOf((*any)(nil)).Elem()).Interface() 156 | } 157 | 158 | require.NoError(t, eval.Evaluate(scope, vPtr)) 159 | 160 | actual := reflect.ValueOf(vPtr).Elem().Interface() 161 | require.Equal(t, tc.expect, actual) 162 | }) 163 | } 164 | } 165 | 166 | func TestVM_Evaluate_Null(t *testing.T) { 167 | expr, err := parser.ParseExpression("null") 168 | require.NoError(t, err) 169 | 170 | eval := vm.New(expr) 171 | 172 | var v interface{} 173 | require.NoError(t, eval.Evaluate(nil, &v)) 174 | require.Nil(t, v) 175 | } 176 | 177 | func TestVM_Evaluate_IdentifierExpr(t *testing.T) { 178 | t.Run("Valid lookup", func(t *testing.T) { 179 | scope := &vm.Scope{ 180 | Variables: map[string]interface{}{ 181 | "foobar": 15, 182 | }, 183 | } 184 | 185 | expr, err := parser.ParseExpression(`foobar`) 186 | require.NoError(t, err) 187 | 188 | eval := vm.New(expr) 189 | 190 | var actual int 191 | require.NoError(t, eval.Evaluate(scope, &actual)) 192 | require.Equal(t, 15, actual) 193 | }) 194 | 195 | t.Run("Invalid lookup", func(t *testing.T) { 196 | expr, err := parser.ParseExpression(`foobar`) 197 | require.NoError(t, err) 198 | 199 | eval := vm.New(expr) 200 | 201 | var v interface{} 202 | err = eval.Evaluate(nil, &v) 203 | require.EqualError(t, err, `1:1: identifier "foobar" does not exist`) 204 | }) 205 | } 206 | 207 | func TestVM_Evaluate_AccessExpr(t *testing.T) { 208 | t.Run("Lookup optional field", func(t *testing.T) { 209 | type Person struct { 210 | Name string `river:"name,attr,optional"` 211 | } 212 | 213 | scope := &vm.Scope{ 214 | Variables: map[string]interface{}{ 215 | "person": Person{}, 216 | }, 217 | } 218 | 219 | expr, err := parser.ParseExpression(`person.name`) 220 | require.NoError(t, err) 221 | 222 | eval := vm.New(expr) 223 | 224 | var actual string 225 | require.NoError(t, eval.Evaluate(scope, &actual)) 226 | require.Equal(t, "", actual) 227 | }) 228 | 229 | t.Run("Invalid lookup 1", func(t *testing.T) { 230 | expr, err := parser.ParseExpression(`{ a = 15 }.b`) 231 | require.NoError(t, err) 232 | 233 | eval := vm.New(expr) 234 | 235 | var v interface{} 236 | err = eval.Evaluate(nil, &v) 237 | require.EqualError(t, err, `1:12: field "b" does not exist`) 238 | }) 239 | 240 | t.Run("Invalid lookup 2", func(t *testing.T) { 241 | _, err := parser.ParseExpression(`{ a = 15 }.7`) 242 | require.EqualError(t, err, `1:11: expected TERMINATOR, got FLOAT`) 243 | }) 244 | 245 | t.Run("Invalid lookup 3", func(t *testing.T) { 246 | _, err := parser.ParseExpression(`{ a = { b = 12 }.7 }.a.b`) 247 | require.EqualError(t, err, `1:17: missing ',' in field list`) 248 | }) 249 | 250 | t.Run("Invalid lookup 4", func(t *testing.T) { 251 | _, err := parser.ParseExpression(`{ a = { b = 12 } }.a.b.7`) 252 | require.EqualError(t, err, `1:23: expected TERMINATOR, got FLOAT`) 253 | }) 254 | } 255 | 256 | func trimWhitespace(in string) string { 257 | f := token.NewFile("") 258 | 259 | s := scanner.New(f, []byte(in), nil, 0) 260 | 261 | var out strings.Builder 262 | 263 | for { 264 | _, tok, lit := s.Scan() 265 | if tok == token.EOF { 266 | break 267 | } 268 | 269 | if lit != "" { 270 | _, _ = out.WriteString(lit) 271 | } else { 272 | _, _ = out.WriteString(tok.String()) 273 | } 274 | } 275 | 276 | return strings.TrimFunc(out.String(), unicode.IsSpace) 277 | } 278 | --------------------------------------------------------------------------------