Skip to content
23 changes: 23 additions & 0 deletions internal/parser/cedar_marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package parser
import (
"bytes"
"fmt"
"io"

"github.com/cedar-policy/cedar-go/internal/consts"
"github.com/cedar-policy/cedar-go/internal/extensions"
Expand All @@ -26,6 +27,28 @@ func (p *Policy) MarshalCedar(buf *bytes.Buffer) {
buf.WriteRune(';')
}

type Encoder struct {
w io.Writer
}

func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w}
}

func (e *Encoder) Encode(p *Policy) error {
var buf bytes.Buffer
p.MarshalCedar(&buf)

buf.WriteByte('\n')

_, err := e.w.Write(buf.Bytes())
if err != nil {
return err
}

return nil
}

// scopeToNode is copied in from eval, with the expectation that
// eval will not be using it in the future.
func scopeToNode(varNode ast.NodeTypeVariable, in ast.IsScopeNode) ast.Node {
Expand Down
55 changes: 55 additions & 0 deletions internal/parser/cedar_marshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package parser_test

import (
"bytes"
"io"
"testing"

"github.com/cedar-policy/cedar-go/internal/parser"
"github.com/cedar-policy/cedar-go/internal/testutil"
"github.com/cedar-policy/cedar-go/x/exp/ast"
)

func TestEncoder(t *testing.T) {
var buf bytes.Buffer

encoder := parser.NewEncoder(&buf)

policy := ast.Permit().
PrincipalEq(johnny).
ActionEq(sow).
ResourceEq(apple).
When(ast.Boolean(true)).
Unless(ast.Boolean(false))

err := encoder.Encode((*parser.Policy)(policy))
testutil.OK(t, err)

const expected = `permit (
principal == User::"johnny",
action == Action::"sow",
resource == Crop::"apple"
)
when { true }
unless { false };
`

testutil.Equals(t, buf.String(), expected)
}

func TestEncoderError(t *testing.T) {
_, w := io.Pipe()
_ = w.Close()

encoder := parser.NewEncoder(w)

policy := ast.Permit().
PrincipalEq(johnny).
ActionEq(sow).
ResourceEq(apple).
When(ast.Boolean(true)).
Unless(ast.Boolean(false))

err := encoder.Encode((*parser.Policy)(policy))
testutil.Error(t, err)
}
6 changes: 5 additions & 1 deletion internal/parser/cedar_tokenize.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ func (t Token) intValue() (int64, error) {
}

func Tokenize(src []byte) ([]Token, error) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine just changing the signature of Tokenize() and forcing callers who have a byte slice to call bytes.NewBuffer() to convert it to an io.Reader. I think there are only two non-test callers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment about a new PR.

return TokenizeReader(bytes.NewBuffer(src))
}

func TokenizeReader(r io.Reader) ([]Token, error) {
var res []Token
var s scanner
s.Init(bytes.NewBuffer(src))
s.Init(r)
for tok := s.nextToken(); s.err == nil && tok.Type != TokenEOF; tok = s.nextToken() {
res = append(res, tok)
}
Expand Down
34 changes: 34 additions & 0 deletions internal/parser/cedar_unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package parser

import (
"fmt"
"io"
"strconv"
"strings"
"sync"

"github.com/cedar-policy/cedar-go/internal/consts"
"github.com/cedar-policy/cedar-go/internal/extensions"
Expand Down Expand Up @@ -33,6 +35,38 @@ func (p *PolicySlice) UnmarshalCedar(b []byte) error {
return nil
}

type Decoder struct {
createParser func() (*parser, error)
}

func NewDecoder(r io.Reader) *Decoder {
Copy link
Copy Markdown
Contributor

@philhassey philhassey Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patjakdev @sagikazarmark How's this for a variation, I think it handles the errors how I want, does what @sagikazarmark needs and avoids using sync like @patjakdev wants:

type Decoder struct {
	r         io.Reader
	parser    *parser
	parserErr error
}

func (d *Decoder) getParser() (*parser, error) {
	if d.parser != nil || d.parserErr != nil {
		return d.parser, d.parserErr
	}
	tokens, err := TokenizeReader(d.r)
	if err != nil {
		d.parserErr = err
		return nil, err
	}
	p := newParser(tokens)
	d.parser = &p
	return d.parser, nil
}

func NewDecoder(r io.Reader) *Decoder {
	return &Decoder{
		r: r,
	}
}

func (d *Decoder) Decode(p *Policy) error {
	parser, err := d.getParser()
	if err != nil {
		return err
	}

	if parser.peek().isEOF() {
		return io.EOF
	}

	return p.fromCedar(parser)
}

Copy link
Copy Markdown
Contributor Author

@sagikazarmark sagikazarmark Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the tokenizer gets rewritten anyway, I don't think it makes much of a difference (except maybe that the current solution is more idiomatic IMO).

I can make the change if you want me to, but my vote goes to the current version.

(I wouldn't mind getting this one merged and iterate later now that I managed to get the CI green)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to be serious about ensuring the same error is always returned from Decode() once an error is encountered, how about something like this?

type Decoder struct {
	r       io.Reader
	parser  *parser
	err     error
}

func NewDecoder(r io.Reader) *Decoder {
	return &Decoder{
		r: r,
	}
}

func (d *Decoder) decode(p *Policy) error {
    if d.parser != nil {
	    tokens, err := TokenizeReader(d.r)
  	    if err != nil {
	        return err
        }
	    parser := newParser(tokens)
        d.parser = &parser
	}

	if d.parser.peek().isEOF() {
		return io.EOF
	}

	return p.fromCedar(d.parser)
}

func (d *Decoder) Decode(p *Policy) error {
    if d.err != nil {
        return d.err
    }

    d.err = p.decode()
    return d.err
}

You could also inline decode() into Decode(), but I thought that ended up with a bit too much indentation for the bulk of the code in the function.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, I'm fine with what you've got right now and happy to merge it.

return &Decoder{
createParser: sync.OnceValues(func() (*parser, error) {
tokens, err := TokenizeReader(r)
if err != nil {
return nil, err
}

p := newParser(tokens)

return &p, nil
}),
}
}

func (d *Decoder) Decode(p *Policy) error {
parser, err := d.createParser()
if err != nil {
return err
}

if parser.peek().isEOF() {
return io.EOF
}

return p.fromCedar(parser)
}

func (p *Policy) UnmarshalCedar(b []byte) error {
tokens, err := Tokenize(b)
if err != nil {
Expand Down
55 changes: 55 additions & 0 deletions internal/parser/cedar_unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package parser_test

import (
"bytes"
"fmt"
"io"
"strings"
"testing"

Expand Down Expand Up @@ -529,6 +531,59 @@ func TestParsePolicySetErrors(t *testing.T) {
}
}

func TestDecoder(t *testing.T) {
policyStr := `permit (
principal,
action,
resource
);
forbid (
principal,
action,
resource
);

`

decoder := parser.NewDecoder(strings.NewReader(policyStr))

var actualPolicy0 parser.Policy
testutil.OK(t, decoder.Decode(&actualPolicy0))

expectedPolicy0 := ast.Permit()
expectedPolicy0.Position = ast.Position{Offset: 0, Line: 1, Column: 1}
testutil.Equals(t, &actualPolicy0, (*parser.Policy)(expectedPolicy0))

var actualPolicy1 parser.Policy
testutil.OK(t, decoder.Decode(&actualPolicy1))

expectedPolicy1 := ast.Forbid()
expectedPolicy1.Position = ast.Position{Offset: 48, Line: 6, Column: 2}
testutil.Equals(t, &actualPolicy1, (*parser.Policy)(expectedPolicy1))

testutil.ErrorIs(t, decoder.Decode(nil), io.EOF)
}

func TestDecoderErrors(t *testing.T) {
t.Parallel()
tests := []string{
`permit("everything");

`,
"okay\x00not okay",
}

for i, tt := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
t.Parallel()
decoder := parser.NewDecoder(strings.NewReader(tt))

testutil.Error(t, decoder.Decode(nil))
testutil.Error(t, decoder.Decode(nil))
})
}
}

func TestParsePolicySet(t *testing.T) {
t.Parallel()
t.Run("single policy", func(t *testing.T) {
Expand Down
53 changes: 53 additions & 0 deletions stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cedar

import (
"io"

"github.com/cedar-policy/cedar-go/ast"
"github.com/cedar-policy/cedar-go/internal/parser"
)

// Encoder encodes [Policy] statements in the human-readable format specified by the [Cedar documentation]
// and writes them to an [io.Writer].
//
// [Cedar documentation]: https://docs.cedarpolicy.com/policies/syntax-grammar.html
type Encoder struct {
enc *parser.Encoder
}

// NewEncoder returns a new [Encoder].
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{enc: parser.NewEncoder(w)}
}

// Encode encodes and writes a single [Policy] statement to the underlying [io.Writer].
func (e *Encoder) Encode(p *Policy) error {
return e.enc.Encode((*parser.Policy)(p.AST()))
}

// Decoder reads, parses and compiles [Policy] statements in the human-readable format specified by the [Cedar documentation]
// from an [io.Reader].
//
// [Cedar documentation]: https://docs.cedarpolicy.com/policies/syntax-grammar.html
type Decoder struct {
dec *parser.Decoder
}

// NewDecoder returns a new [Decoder].
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{dec: parser.NewDecoder(r)}
}

// Decode parses and compiles a single [Policy] statement from the underlying [io.Reader].
func (e *Decoder) Decode(p *Policy) error {
var parserPolicy parser.Policy

err := e.dec.Decode(&parserPolicy)
if err != nil {
return err
}

*p = *NewPolicyFromAST((*ast.Policy)(&parserPolicy))

return nil
}
Loading