Skip to content

Commit

Permalink
Implement Core Functionality (#1)
Browse files Browse the repository at this point in the history
* Initialise Project

- Added go.mod for Go project
- Added an image asset and updated README with simple description
- Updated ignore list to ignore IDE settings

* Implement Varint Handling

- Added a module varint.go with functions for sizing, encoding and decoding varints.
- Encode can be performed with an allocation with encodeVarint, while appendVarint can be used to for appending to an already allocated buffer
- Decode can only performed from ByteReader by consuming it incrementally with consumeVarint
- Added varint decode errors in errors.go
- Added varint test cases in varint_test.go
- Added testify/assert and google/go-fuzz as testing dependencies. (Subsequently modified the go.mod and go.sum files)

* Add WireType Handling

- Added a module wiretype.go with the WireType enum
- Added methods on WireType to check if it is null or compound
- Added wiretype test cases in wiretype_test.go

* Add Encoding Buffer Type

- Added a writebuffer type for write-only bytes handling while encoding

* Implement Encoding Functionality

- Added polorize() and set of polorizeType functions to encode various datatypes into their POLO form
- Added Polorize() exported function for attempting to encode an arbitrary Go object into its POLO form

* Implement Decode Buffer Types

- Added readbuffer and loadreader types for read-only bytes handling while decoding
- Added a custom error MalformedTagError for handling malformed varint tags encountered while decoding

* Implement Decoding Functionality

- Added depolorize() and set of depolorizeType functions to decode various datatypes from their POLO form
- Added Depolorize() exported function for attempting to decode a POLO wire into a Go object
- Added new custom errors IncompatibleWireError and DecodeError

* Add POLO Test Cases

- Added test cases in polo_test.go
- Test cases check Polorize and Depolorize behaviour and implicitly check all buffer and helper functions
- Test cases check for malformed wire error messages

* Add Benchmarks and Examples

- Added a simple benchmark function in bench_test.go
- Added simple example functions in example_test.go

* Update README

- Added Examples and Features Documentation in README.md
  • Loading branch information
sarvalabs-manish authored Nov 23, 2022
1 parent 94039b2 commit b1e51dc
Show file tree
Hide file tree
Showing 17 changed files with 2,722 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@

# Dependency directories (remove the comment below to include it)
# vendor/

# IDE Settings
.idea
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,61 @@
# go-polo
![image](./banner.png)

# go-polo
**go-polo** is the Go implementation of the POLO Serialization Format.

**POLO** stands for *Prefix Ordered Lookup Offset*. It is meant for use in projects that prioritize
deterministic serialization, minimal wire size and code safety. POLO follows a very strict specification
that is optimized for lookups and differential messaging. The full POLO Wire Specification can be found [here](https://github.com/sarvalabs/polo).

### Features
- Deterministic Serialization
- Lookup Optimized Binary Wire Format
- Partial Element Deserialization *(Coming Soon)*
- Differential Messaging *(Coming Soon)*

### Usage Examples

```go
package main

import (
"fmt"

"github.com/sarvalabs/go-polo"
)

// Fruit is an example struct
type Fruit struct {
Name string
Cost int
Alias []string
}

func main() {
// Declare a Fruit object
orange := &Fruit{"orange", 300, []string{"tangerine", "mandarin"}}

// Polorize the object
wire, err := polo.Polorize(orange)
if err != nil {
panic(err)
}

fmt.Println(wire)

// Declare a new Fruit object
newfruit := new(Fruit)
// Depolorize the Fruit object
if err := polo.Depolorize(newfruit, wire); err != nil {
panic(err)
}

fmt.Println(newfruit)
}

// Output:
// [143 4 78 100 34 182 2 111 114 97 110 103 101 1 44 62 148 1 100 116 97 110 103 101 114 105 110 101 111 114 97 110 103 101]
// &{orange 300 [tangerine mandarin]}
```

Check out more [examples](./example_test.go) here.
Binary file added banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package polo

import "testing"

type MixedObject struct {
A string
B int32
C []string
D map[string]string
E float64
}

func BenchmarkMixed(b *testing.B) {
object := MixedObject{
A: "Sins & Virtues",
B: 567822,
C: []string{"pride", "greed", "lust", "gluttony", "envy", "wrath", "sloth"},
D: map[string]string{"bravery": "piety", "friendship": "chastity"},
E: 45.23,
}

wire, _ := Polorize(object)
newObject := new(MixedObject)

b.ResetTimer()

b.Run("Polorize", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = Polorize(object)
}
})

b.Run("Depolorize", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Depolorize(newObject, wire)
}
})
}
207 changes: 207 additions & 0 deletions buffers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package polo

import (
"bytes"
"errors"
"fmt"
"io"
)

// writebuffer is a write-only byte buffer that appends to a head and body
// buffer simultaneously. It can be instantiated without a constructor
type writebuffer struct {
head []byte
body []byte
offset uint64
}

// write appends v to the body of the writebuffer and a varint tag describing its offset and the given WireType.
// The writebuffer.offset value is incremented by the length of v after both buffers have been updated.
func (wb *writebuffer) write(w WireType, v []byte) {
wb.head = appendVarint(wb.head, (wb.offset<<4)|uint64(w))
wb.body = append(wb.body, v...)
wb.offset += uint64(len(v))
}

// bytes returns the contents of the writebuffer as a single slice of bytes.
// The returned bytes is the head followed by the body of the writebuffer.
func (wb writebuffer) bytes() []byte {
return append(wb.head, wb.body...)
}

// load returns contents of the writebuffer as a slice of bytes tagged by a WireLoad,
// i.e, a load key with the length of the head is prefixed before the head followed by the body.
func (wb writebuffer) load() (buf []byte) {
key := (uint64(len(wb.head)) << 4) | uint64(WireLoad)
size := len(wb.head) + len(wb.body) + sizeVarint(key)

buf = appendVarint(make([]byte, 0, size), key)
buf = append(buf, wb.head...)
buf = append(buf, wb.body...)

return
}

// readbuffer is a read-only buffer that is obtained from a single tag and its body.
type readbuffer struct {
wire WireType
data []byte
}

// newreadbuffer creates a new readbuffer from a given slice of bytes b.
// Returns a readbuffer and an error if one occurs.
// Throws an error if the tag is malformed.
func newreadbuffer(b []byte) (readbuffer, error) {
// Create a reader from b
r := bytes.NewReader(b)

// Attempt to consume a varint from the reader
tag, consumed, err := consumeVarint(r)
if err != nil {
return readbuffer{}, MalformedTagError{err.Error()}
}

// Create a readbuffer from the wiretype of the tag (first 4 bits)
return readbuffer{WireType(tag & 15), b[consumed:]}, nil
}

// bytes returns the full readbuffer as slice of bytes.
// It prepends its wiretype to rest of the data.
func (rb readbuffer) bytes() []byte {
return prepend(byte(rb.wire), rb.data)
}

// load returns a loadreader from a readbuffer.
// Throws an error if the wiretype of the readbuffer is not compound (pack)
func (rb *readbuffer) load() (*loadreader, error) {
// Check that readbuffer has a compound wiretype
if !rb.wire.IsCompound() {
return nil, errors.New("load convert fail: not a compound wire")
}

// Create a reader from the readbuffer data
r := bytes.NewReader(rb.data)

// Attempt to consume a varint from the reader for the load tag
loadtag, _, err := consumeVarint(r)
if err != nil {
return nil, fmt.Errorf("load convert fail: %w", MalformedTagError{err.Error()})
}

// Check that the tag has a type of WireLoad
if loadtag&15 != uint64(WireLoad) {
return nil, errors.New("load convert fail: missing load tag")
}

// Read the number of bytes specified by the load for the header
head, err := read(r, int(loadtag>>4))
if err != nil {
return nil, fmt.Errorf("load convert fail: missing head: %w", err)
}

// Read the remaining bytes in the reader for the body
body, _ := read(r, r.Len())

// Create a new loadreader and return it
lr := newloadreader(head, body)

return lr, nil
}

// loadreader is a read-only buffer that is obtained from a compound wire (pack).
// Iteration over the loadreader will return elements from the load one by one.
type loadreader struct {
head *bytes.Reader
body []byte

coff int // represents the offset position for the current element
noff int // represents the offset position for the next element

cw WireType // represents the wiretype of the current element
nw WireType // represents the wiretype of the next element
}

// newloadreader creates a new loadreader for a given head and body slice of bytes.
// The returned loadreader is seeded and the next iteration will return the first element of the load.
func newloadreader(head, body []byte) *loadreader {
// Initialize an empty loadreader
lr := new(loadreader)

// Create a reader from the head data and set it
lr.head = bytes.NewReader(head)
lr.body = body

// Seed the offset values of the loadreader by iterating once
_, _ = lr.next()

return lr
}

// done returns whether all elements in the loadreader have been read.
func (lr *loadreader) done() bool {
// loadreader is done if the noff is set to -1
return lr.noff == -1
}

// next returns the next element from the loadreader.
// Returns an error if loadreader is done. (can be checked with a call to done())
func (lr *loadreader) next() (readbuffer, error) {
// Check if the head reader is exhausted
if lr.head.Len() == 0 {
// Check if load reader is done
if lr.done() {
return readbuffer{}, errors.New("loadreader exhausted")
}

// Update current values from the next values
lr.coff, lr.cw = lr.noff, lr.nw
// Update next values to nulls. -1 means the loadreader is set as done
lr.noff, lr.nw = -1, WireNull

// Create a readbuffer from the current wiretype and the rest of data in the body and return it
return readbuffer{lr.cw, lr.body[lr.coff:]}, nil
}

// Attempt to consume a varint from the head reader
tag, _, err := consumeVarint(lr.head)
if err != nil {
return readbuffer{}, MalformedTagError{err.Error()}
}

// Update the current values from the next values
lr.coff, lr.cw = lr.noff, lr.nw
// Set the next values based on the tag data (first 4 bits represent wiretype, rest the offset position of the dats)
lr.noff, lr.nw = int(tag>>4), WireType(tag&15)

// Create a readbuffer from the current wiretype and body bytes between the two offset positions
return readbuffer{lr.cw, lr.body[lr.coff:lr.noff]}, nil
}

// read consumes n number of bytes from the reader and returns it as a slice of bytes.
// Throws an error if the r does not have n number of bytes.
func read(r io.Reader, n int) ([]byte, error) {
// If n == 0, return an empty byte slice
if n == 0 {
return []byte{}, nil
}

// Create a slice of bytes with the specified length
d := make([]byte, n)

// Read from the reader into d
if rn, err := r.Read(d); err != nil || rn != n {
return nil, errors.New("insufficient data in reader")
}

return d, nil
}

// prepend is a generic function that accepts an object of some any type and a slice of objects of
// the same type and inserts the object to the front of the slice and shifts the other elements.
func prepend[Element any](y Element, x []Element) []Element {
x = append(x, *new(Element))
copy(x[1:], x)
x[0] = y

return x
}
Loading

0 comments on commit b1e51dc

Please sign in to comment.