Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions packages/cel-proto-parser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# cel-proto-parser

CEL (Common Expression Language) proto parser and deparser for TypeScript.

## Overview

This package provides tools for working with CEL (Common Expression Language) Abstract Syntax Trees (ASTs):

1. **Proto Parser**: Parse CEL protobuf definitions to generate TypeScript types
2. **Deparser**: Convert CEL AST back to CEL expression strings
3. **AST Helpers**: Factory functions for constructing CEL AST nodes

## Installation

```bash
npm install cel-proto-parser
```

## Usage

### Deparser

Convert CEL AST to expression strings:

```typescript
import { deparse, Expr } from 'cel-proto-parser';

// Simple expression: x > 5
const expr: Expr = {
callExpr: {
function: '_>_',
args: [
{ identExpr: { name: 'x' } },
{ constExpr: { int64Value: 5 } }
]
}
};

console.log(deparse(expr)); // Output: "x > 5"
```

### Proto Parser

Generate TypeScript types from CEL proto files:

```typescript
import { CelProtoParser } from 'cel-proto-parser';

const parser = new CelProtoParser('path/to/syntax.proto', {
outDir: './generated',
types: { enabled: true },
enums: { enabled: true },
utils: { astHelpers: { enabled: true } },
deparser: { enabled: true }
});

parser.write();
```

## CEL Expression Types

The deparser supports all CEL expression types:

- **Constants**: `null`, `true`, `false`, integers, floats, strings, bytes
- **Identifiers**: `request`, `user`, etc.
- **Field Access**: `request.auth.claims`
- **Function Calls**: `size(list)`, `str.startsWith("prefix")`
- **Operators**: `+`, `-`, `*`, `/`, `%`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `!`, `-` (unary)
- **Ternary**: `condition ? trueExpr : falseExpr`
- **Index Access**: `list[0]`, `map["key"]`
- **Lists**: `[1, 2, 3]`
- **Maps**: `{"key": value}`
- **Message Construction**: `MyMessage{field: value}`
- **Macros**: `has()`, `all()`, `exists()`, etc.

## API Reference

### `deparse(expr: Expr, options?: DeparserOptions): string`

Converts a CEL AST expression to a string.

Options:
- `spaces`: Whether to add spaces around operators (default: `true`)

### `CelProtoParser`

Parses CEL proto files and generates TypeScript code.

Constructor options:
- `outDir`: Output directory for generated files
- `types.enabled`: Generate TypeScript interfaces
- `enums.enabled`: Generate TypeScript enums
- `utils.astHelpers.enabled`: Generate AST helper functions
- `deparser.enabled`: Generate deparser module

## License

MIT
34 changes: 34 additions & 0 deletions packages/cel-proto-parser/__fixtures__/expressions/basic.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Basic CEL Expression Fixtures for Round-Trip Testing
# A small handful of representative expressions

# Simple values
42
3.14
true
"hello"

# Field access
request.auth
request.auth.claims.email

# Arithmetic
1 + 2
a * b + c

# Comparison
x == y
size(list) > 0

# Logical
a && b
!enabled

# Ternary
x ? 1 : 2

# Collections
[1, 2, 3]
{"key": "value"}

# Function calls
size(items)
111 changes: 111 additions & 0 deletions packages/cel-proto-parser/__fixtures__/expressions/cel-expressions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# CEL Expression Fixtures
# Each line is a CEL expression to test for round-trip parsing
# Lines starting with # are comments

# Literals
1
42
-5
3.14
-2.5
1.0
true
false
null
"hello"
"hello world"
"with \"quotes\""
"with\nnewline"
""

# Unsigned integers
1u
42u
0u

# Identifiers
x
foo
request
_underscore
camelCase

# Field access
request.auth
request.auth.claims
request.auth.claims.email
a.b.c.d

# Arithmetic operators
1 + 2
a + b
x - y
a * b
x / y
a % b
1 + 2 * 3
(1 + 2) * 3

# Comparison operators
a == b
x != y
a < b
a <= b
a > b
a >= b

# Logical operators
a && b
x || y
!a
a && b && c
a || b || c
a && b || c
(a || b) && c

# Ternary conditional
x ? 1 : 2
a == b ? "yes" : "no"
x > 0 ? x : -x

# List literals
[]
[1]
[1, 2, 3]
[a, b, c]
["a", "b", "c"]

# Map literals
{}
{"a": 1}
{"a": 1, "b": 2}
{1: "one", 2: "two"}

# Index access
list[0]
map["key"]
a[b]
list[i + 1]

# Function calls
size(list)
int(x)
string(42)
type(x)

# Method calls
list.size()
str.contains("test")
list.map(x, x * 2)

# In operator
x in list
"admin" in roles
key in map

# Complex expressions
request.auth.claims.email == "admin@example.com"
size(request.body) > 0 && request.method == "POST"
user.age >= 18 && "admin" in user.roles
items.all(x, x > 0)
items.exists(x, x == target)
57 changes: 57 additions & 0 deletions packages/cel-proto-parser/__fixtures__/expressions/policy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Policy-Shaped CEL Expression Fixtures
# OPA-ish patterns useful for Kubernetes admission control
# Rich in syntax variety for deparser testing

# 1) Core allow/deny shapes
request.user == "system:admin"
request.user in ["alice", "bob", "carol"]
request.verb in ["create", "update"] && request.resource == "pods"
!(request.user.startsWith("system:"))
request.namespace != "kube-system" && request.namespace != "kube-public"

# 2) Field presence / null-ish handling
has(object.metadata.labels) && has(object.metadata.labels["app"])
!has(object.spec.replicas) || object.spec.replicas <= 10
has(object.spec.template) ? object.spec.template.spec.nodeName == "" : true
has(object.spec.securityContext) && has(object.spec.securityContext.runAsNonRoot) && object.spec.securityContext.runAsNonRoot == true
has(params.exemptUsers) && request.user in params.exemptUsers

# 3) Map + list literals, indexing, membership
object.metadata.labels["team"] in ["infra", "platform"]
object.metadata.annotations["owner"] == request.user
size(object.spec.containers) > 0
object.spec.containers[0].image.matches("^ghcr\\.io/constructive-io/.+")

# 4) String ops + regex escaping edge cases
request.user.matches("^user:[a-z0-9_-]{3,32}$")
object.metadata.name.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
object.spec.containers.exists(c, c.name.startsWith("sidecar-") && c.image.contains("@sha256:"))
object.spec.serviceAccountName != "" && !object.spec.serviceAccountName.startsWith("default")

# 5) Quantifiers/macros (great for AST/deparser)
object.spec.containers.all(c, !c.securityContext.privileged)
object.spec.containers.exists(c, has(c.resources.limits["cpu"]) && has(c.resources.limits["memory"]))
object.spec.containers.filter(c, c.image.contains(":latest")).size() == 0
object.spec.containers.map(c, c.name).exists(n, n == "app")

# 6) Nested logic + precedence stress tests
request.verb == "update" && (request.user == "alice" || request.user == "bob") && !request.dryRun
(request.user in params.admins) || (request.namespace == "dev" && request.user.matches("^user:dev_.*$"))
!(request.verb == "delete" && request.resource == "namespaces")

# 7) Numeric + bounds + ternary
has(object.spec.replicas) ? (object.spec.replicas >= 1 && object.spec.replicas <= 20) : true
object.spec.containers.all(c, c.resources.requests["cpu"] <= c.resources.limits["cpu"])

# 8) "OPA-ish" patterns (rule-ish in one expression)
request.user in params.admins || (request.verb in ["get","list","watch"] && request.namespace in params.readNamespaces)
request.user == object.metadata.annotations["owner"] || request.user in params.delegates[object.metadata.annotations["owner"]]

# 9) Equality against structured literals (map/list)
object.metadata.labels == {"app": "api", "tier": "backend"}
object.spec.tolerations.exists(t, t == {"key":"dedicated","operator":"Equal","value":"gpu","effect":"NoSchedule"})

# 10) "Weird but valid" deparser edge cases
"" == ""
true ? false ? true : false : true
((a + b) * c) / (d - 1) > 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# RLS Policy Template CEL Expressions
# Maps to common policy templates: direct_owner, membership, permission bits, etc.
# Uses: row (table row), auth (auth context), acl (ACL table view), params (compile-time params)

# 1) direct_owner (row.owner_id == user_id)
row.owner_id == auth.user_id
has(row.owner_id) && row.owner_id == auth.user_id

# 2) multi_owner / any_owner (row.sender_id OR row.receiver_id)
auth.user_id in [row.sender_id, row.receiver_id]

# 3) membership_by_field (row.org_id exists in ACL for user)
acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "org" && m.entity_id == row.org_id)

# 4) membership_by_join (row -> join table -> memberships)
acl.joins.exists(j, j.row_id == row.id && acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "group" && m.entity_id == j.entity_id))

# 5) scoped_owner / scope_owner (membership exists AND matches scope)
acl.memberships.exists(m, m.user_id == auth.user_id && m.scope == "project" && m.entity_id == row.project_id)
acl.memberships.exists(m, m.user_id == auth.user_id && m.membership_type == params.membership_type && m.entity_id == row.entity_id)

# 6) hasAccess / permission-bit checks (bitmask)
# NOTE: @marcbachmann/cel-js does not support bitwise AND (&) operator
# These would be: acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_id == row.org_id && (m.perms & params.required_perm) != 0)

# 7) Role-based overrides (auth.roles)
("admin" in auth.roles) || (row.owner_id == auth.user_id)
auth.user.startsWith("system:") || row.owner_id == auth.user_id

# 8) acl_exists (no entity match; "member of ANY org")
acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "org")

# 9) relatedAccess / relationship ownership (row references another object)
acl.parents.exists(p, p.id == row.parent_id && (p.owner_id == auth.user_id || acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_id == p.org_id)))

# 10) Array membership / group array checks
has(row.allowed_user_ids) && auth.user_id in row.allowed_user_ids
row.group_ids.exists(gid, acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "group" && m.entity_id == gid))

# 11) "Owner OR membership" compound policy (very common)
row.owner_id == auth.user_id || acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_id == row.org_id)

# 12) Insert/update "WITH CHECK" style (new row constraints)
row.owner_id == auth.user_id
newRow.owner_id == auth.user_id
request.verb == "update" ? (newRow.owner_id == oldRow.owner_id) : true
Loading