Skip to content

OTServer - Adding AST Based Document Representation #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 29, 2022
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
10 changes: 3 additions & 7 deletions backend/editor/OT/data/jsonConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strconv"

"cms.csesoc.unsw.edu.au/editor/OT/data/datamodels/cmsmodel"
"cms.csesoc.unsw.edu.au/internal/cmsjson"
"cms.csesoc.unsw.edu.au/pkg/cmsjson"
)

// models contains all the data models for the editor
Expand Down Expand Up @@ -44,12 +44,8 @@ func parseDataGivenType(dataStr string, dataType string) (interface{}, error) {
case "string":
return dataStr, nil
case "component":
var result interface{}
if err := cmsJsonConf.Unmarshall([]byte(dataStr), &result); err != nil {
return nil, err
}

return result, nil
// todo: later
return nil, nil
}
return nil, errors.New("unable to parse data type")
}
13 changes: 8 additions & 5 deletions backend/editor/OT/data/operationModel.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package data

import "errors"
import (
"errors"

"cms.csesoc.unsw.edu.au/pkg/cmsjson"
)

type (
OperationType int
Expand Down Expand Up @@ -72,10 +76,9 @@ func (a ArrayEdit) GetType() OperationType { return ArrayEditType }
// Parse is a utility function that takes a JSON stream and parses the input into
// a Request object
func Parse(request string) (OperationRequest, error) {
requestObject := OperationRequest{}
if err := cmsJsonConf.Unmarshall([]byte(request), &requestObject); err != nil {
if operation, err := cmsjson.Unmarshall[OperationRequest](cmsJsonConf, []byte(request)); err != nil {
return OperationRequest{}, errors.New("invalid request format")
} else {
return *operation, nil
}

return requestObject, nil
}
56 changes: 56 additions & 0 deletions backend/pkg/cmsjson/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# CMSJson

CMSJson is our own custom in-house JSON marshalling library, the library marshalls JSON into an AST and verifies that the incoming JSON conforms to a specific type. After parsing and marshalling the generated AST supports the type-safe extension of the AST and allows the addition of substructures to the AST, the validity of these insertion/deletion operations is defined by the type provided when marshalling the type.

## Usage
The library is relatively simple to use, we marshall un-marshal JSON data by provided a type template and a place to output it, eg:
```go
type (
myTemplateInner struct {
hello string
}

myTemplate struct {
x int
y string
inner myTemplateInner
}
)

func main() {
// There's two ways to use this library
// 1. AST mode
// 2. Un-marshall mode
// - The un-marshall mode can un-marshall a struct directly
// - If the output type specifies "ASTNode" however the will partially un-marshall the output

// 1. AST MODE
ast := cmsjson.UnmarshallAST[myTemplate](`
{
"x": 3,
"y": "hello",
inner: {
"hello": "world"
}
}
`)

// outputs the AST nice a nice format
fmt.Printf("AST: %s", ast.Stringify())

// 2. Un-marshall mode
// directly to a struct
var dest = myTemplate{}
cmsjson.Unmarshall[myTemplate](&dest)

// directly to a struct with an AST type
var dest = struct{
x int
y string
inner cmsjson.AstNode
}{}

cmsjson.Unmarshall[myTemplate](&dest)
}
```
When un-marshalling into a type that contains an ASTNode the outputted value is the AST decomposition of the requested field.
175 changes: 175 additions & 0 deletions backend/pkg/cmsjson/ast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cmsjson

import (
"fmt"
"reflect"
)

type (
// jsonNode is the internal implementation of AstNode, *jsonNode @implements AstNode
// AstNode is a simple interface that represents a node in our JSON AST, we have a few important constraints that should be enforced by any implementation of the AstNode, those constraints are:
// - An ASTNode is either a: JsonPrimitive, JsonObject or a JsonArray
// - GetKey can return nil indicating that it is JUST a value
// - Since a node can be either a JsonPrimitive, JsonObject or a JsonArray:
// - 2 of the three functions: JsonPrimitive(), JsonObject(), JsonArray() will return nil (indicating the node is not of that type) while one will return an actual value
// - We are guaranteed that one of these functions will return a value
// - All implementations of AstNode must conform to this specification (there is no way within the Go type system to enforce this unfortunately :( )
// - Note that the reflect.Type returned by JsonArray is the type of the array, ie if it was an array of integers then the reflect.type is an integer
// - Note that jsonNode implements AstNode (indirectly), AstNode is of the form:
// AstNode interface {
// GetKey() string
//
// JsonPrimitive() (interface{}, reflect.Type)
// JsonObject() ([]AstNode, reflect.Type)
// JsonArray() ([]AstNode, reflect.Type)
// }
jsonNode struct {
// key could be nil (according to the AstNode definition)
key string

// either value or children can be nil (according to the AstNode definition)
value interface{}
children []*jsonNode

// underlying type is the type modelled by this jsonNode, isObject allows us distinguish between arrays and objects
underlyingType reflect.Type
isObject bool
}

// jsonPrimitives is a generic constraint for json primitive values
jsonPrimitives interface {
~int | ~float64 | ~bool | ~string
}
)

// Interface implementations for AstNode

// GetKey returns the key of the underlying jsonNode
func (node *jsonNode) GetKey() string { return node.key }

// JsonPrimitive returns the underlying primitive value in a jsonNode, it either returns the value or nil in accordance with the
// definition of the AstNode
func (node *jsonNode) GetPrimitive() (interface{}, reflect.Type) {
node.validateNode()
if node.value != nil {
return node.value, node.underlyingType
}

return nil, nil
}

// JsonObject returns the underlying json object in a jsonNode, it either returns the value or nil in accordance with the
// definition of the AstNode
func (node *jsonNode) JsonObject() ([]*jsonNode, reflect.Type) {
node.validateNode()
if node.children != nil && node.isObject {
return node.children, node.underlyingType
}

return nil, nil
}

// JsonArray returns the underlying json array in a jsonNode, it either returns the value or nil in accordance with the
// definition of the AstNode
func (node *jsonNode) JsonArray() ([]*jsonNode, reflect.Type) {
node.validateNode()
if node.children != nil && !node.isObject {
return node.children, node.underlyingType
}

return nil, nil
}

// validateNode determines if the current node configuration was corrupted or not
func (node *jsonNode) validateNode() {
if (node.value == nil && node.children == nil) || (node.value != nil && node.children != nil) {
panic(fmt.Errorf("the provided error configuration: %v was corrupted somehow", *node))
}
}

// General functions for creating instances of jsonNode

// newJsonArray constructs a new instance of a JsonArray given the array of json values it contains
// note that there is no validation to ensure that the fields match the incoming
// underlyingType
func newJsonArray(key string, values []*jsonNode, underlyingType reflect.Type) *jsonNode {
return &jsonNode{
key: key,
value: nil,

children: values,
underlyingType: underlyingType,
isObject: false,
}
}

// newJsonObject instantiates a new instance of a JsonObject type, note that there is no validation to ensure that the fields match the incoming
// underlyingType
func newJsonObject(key string, values []*jsonNode, underlyingType reflect.Type) *jsonNode {
return &jsonNode{
key: key,
value: nil,

children: values,
underlyingType: underlyingType,
isObject: true,
}
}

// newJsonPrimitive instantiates a new instance of a jsonPrimitive type, note that this method has no validation logic (perhaps we can add it in the future)
func newJsonPrimitive(key string, value interface{}, underlyingType reflect.Type) *jsonNode {
return &jsonNode{
key: key,
value: value,

children: nil,
underlyingType: underlyingType,
isObject: false,
}
}

// InsertOrUpdate inserts a secondary json node into a jsonNode given the index in which it needs to be inserted, note that it also does type validation :D
func (node *jsonNode) InsertOrUpdate(toInsert *jsonNode, location int) error {
node.validateNode()
if node.children != nil {
return fmt.Errorf("node is a terminal primitive value %v, primitive values cannot have children", *node)
}

// validInsertions are characterized by inserting into a struct at the correct type of
validInsert := (getStructFieldType(node.underlyingType, location) == toInsert.underlyingType && location < len(node.children)) ||
(node.underlyingType == toInsert.underlyingType && location <= len(node.children))

if validInsert {
switch location {
case len(node.children):
node.children = append(node.children, toInsert)
default:
node.children = append(append(node.children[:location], toInsert), node.children[location:]...)
}

return nil
}

return fmt.Errorf("the insertion for %v index %d was invalid", *node, location)
}

// NewPrimitiveFromValue constructs a new jsonNode from a primitive value
func NewPrimitiveFromValue[T jsonPrimitives](key string, value T) *jsonNode {
return &jsonNode{
key: key,
value: value,

children: nil,
underlyingType: reflect.TypeOf(value),
isObject: false,
}
}

// getStructFieldType fetches the field type of a struct given its index
func getStructFieldType(structType reflect.Type, index int) reflect.Type {
if structType.Kind() != reflect.Struct {
return nil
}

return structType.FieldByIndex([]int{index}).Type
}
Loading