Skip to content

Commit 6146a42

Browse files
authored
OTServer - Adding AST Based Document Representation (#199)
* added AST type + type verification * added ast documentatio * added AST unmarshaller * added unmarshaller code * moved cmsjson references to pkg
1 parent cad6971 commit 6146a42

File tree

9 files changed

+420
-45
lines changed

9 files changed

+420
-45
lines changed

backend/editor/OT/data/jsonConfig.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"strconv"
77

88
"cms.csesoc.unsw.edu.au/editor/OT/data/datamodels/cmsmodel"
9-
"cms.csesoc.unsw.edu.au/internal/cmsjson"
9+
"cms.csesoc.unsw.edu.au/pkg/cmsjson"
1010
)
1111

1212
// models contains all the data models for the editor
@@ -44,12 +44,8 @@ func parseDataGivenType(dataStr string, dataType string) (interface{}, error) {
4444
case "string":
4545
return dataStr, nil
4646
case "component":
47-
var result interface{}
48-
if err := cmsJsonConf.Unmarshall([]byte(dataStr), &result); err != nil {
49-
return nil, err
50-
}
51-
52-
return result, nil
47+
// todo: later
48+
return nil, nil
5349
}
5450
return nil, errors.New("unable to parse data type")
5551
}

backend/editor/OT/data/operationModel.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package data
22

3-
import "errors"
3+
import (
4+
"errors"
5+
6+
"cms.csesoc.unsw.edu.au/pkg/cmsjson"
7+
)
48

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

backend/pkg/cmsjson/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# CMSJson
2+
3+
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.
4+
5+
## Usage
6+
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:
7+
```go
8+
type (
9+
myTemplateInner struct {
10+
hello string
11+
}
12+
13+
myTemplate struct {
14+
x int
15+
y string
16+
inner myTemplateInner
17+
}
18+
)
19+
20+
func main() {
21+
// There's two ways to use this library
22+
// 1. AST mode
23+
// 2. Un-marshall mode
24+
// - The un-marshall mode can un-marshall a struct directly
25+
// - If the output type specifies "ASTNode" however the will partially un-marshall the output
26+
27+
// 1. AST MODE
28+
ast := cmsjson.UnmarshallAST[myTemplate](`
29+
{
30+
"x": 3,
31+
"y": "hello",
32+
inner: {
33+
"hello": "world"
34+
}
35+
}
36+
`)
37+
38+
// outputs the AST nice a nice format
39+
fmt.Printf("AST: %s", ast.Stringify())
40+
41+
// 2. Un-marshall mode
42+
// directly to a struct
43+
var dest = myTemplate{}
44+
cmsjson.Unmarshall[myTemplate](&dest)
45+
46+
// directly to a struct with an AST type
47+
var dest = struct{
48+
x int
49+
y string
50+
inner cmsjson.AstNode
51+
}{}
52+
53+
cmsjson.Unmarshall[myTemplate](&dest)
54+
}
55+
```
56+
When un-marshalling into a type that contains an ASTNode the outputted value is the AST decomposition of the requested field.

backend/pkg/cmsjson/ast.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package cmsjson
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
)
7+
8+
type (
9+
// jsonNode is the internal implementation of AstNode, *jsonNode @implements AstNode
10+
// 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:
11+
// - An ASTNode is either a: JsonPrimitive, JsonObject or a JsonArray
12+
// - GetKey can return nil indicating that it is JUST a value
13+
// - Since a node can be either a JsonPrimitive, JsonObject or a JsonArray:
14+
// - 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
15+
// - We are guaranteed that one of these functions will return a value
16+
// - All implementations of AstNode must conform to this specification (there is no way within the Go type system to enforce this unfortunately :( )
17+
// - 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
18+
// - Note that jsonNode implements AstNode (indirectly), AstNode is of the form:
19+
// AstNode interface {
20+
// GetKey() string
21+
//
22+
// JsonPrimitive() (interface{}, reflect.Type)
23+
// JsonObject() ([]AstNode, reflect.Type)
24+
// JsonArray() ([]AstNode, reflect.Type)
25+
// }
26+
jsonNode struct {
27+
// key could be nil (according to the AstNode definition)
28+
key string
29+
30+
// either value or children can be nil (according to the AstNode definition)
31+
value interface{}
32+
children []*jsonNode
33+
34+
// underlying type is the type modelled by this jsonNode, isObject allows us distinguish between arrays and objects
35+
underlyingType reflect.Type
36+
isObject bool
37+
}
38+
39+
// jsonPrimitives is a generic constraint for json primitive values
40+
jsonPrimitives interface {
41+
~int | ~float64 | ~bool | ~string
42+
}
43+
)
44+
45+
// Interface implementations for AstNode
46+
47+
// GetKey returns the key of the underlying jsonNode
48+
func (node *jsonNode) GetKey() string { return node.key }
49+
50+
// JsonPrimitive returns the underlying primitive value in a jsonNode, it either returns the value or nil in accordance with the
51+
// definition of the AstNode
52+
func (node *jsonNode) GetPrimitive() (interface{}, reflect.Type) {
53+
node.validateNode()
54+
if node.value != nil {
55+
return node.value, node.underlyingType
56+
}
57+
58+
return nil, nil
59+
}
60+
61+
// JsonObject returns the underlying json object in a jsonNode, it either returns the value or nil in accordance with the
62+
// definition of the AstNode
63+
func (node *jsonNode) JsonObject() ([]*jsonNode, reflect.Type) {
64+
node.validateNode()
65+
if node.children != nil && node.isObject {
66+
return node.children, node.underlyingType
67+
}
68+
69+
return nil, nil
70+
}
71+
72+
// JsonArray returns the underlying json array in a jsonNode, it either returns the value or nil in accordance with the
73+
// definition of the AstNode
74+
func (node *jsonNode) JsonArray() ([]*jsonNode, reflect.Type) {
75+
node.validateNode()
76+
if node.children != nil && !node.isObject {
77+
return node.children, node.underlyingType
78+
}
79+
80+
return nil, nil
81+
}
82+
83+
// validateNode determines if the current node configuration was corrupted or not
84+
func (node *jsonNode) validateNode() {
85+
if (node.value == nil && node.children == nil) || (node.value != nil && node.children != nil) {
86+
panic(fmt.Errorf("the provided error configuration: %v was corrupted somehow", *node))
87+
}
88+
}
89+
90+
// General functions for creating instances of jsonNode
91+
92+
// newJsonArray constructs a new instance of a JsonArray given the array of json values it contains
93+
// note that there is no validation to ensure that the fields match the incoming
94+
// underlyingType
95+
func newJsonArray(key string, values []*jsonNode, underlyingType reflect.Type) *jsonNode {
96+
return &jsonNode{
97+
key: key,
98+
value: nil,
99+
100+
children: values,
101+
underlyingType: underlyingType,
102+
isObject: false,
103+
}
104+
}
105+
106+
// newJsonObject instantiates a new instance of a JsonObject type, note that there is no validation to ensure that the fields match the incoming
107+
// underlyingType
108+
func newJsonObject(key string, values []*jsonNode, underlyingType reflect.Type) *jsonNode {
109+
return &jsonNode{
110+
key: key,
111+
value: nil,
112+
113+
children: values,
114+
underlyingType: underlyingType,
115+
isObject: true,
116+
}
117+
}
118+
119+
// 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)
120+
func newJsonPrimitive(key string, value interface{}, underlyingType reflect.Type) *jsonNode {
121+
return &jsonNode{
122+
key: key,
123+
value: value,
124+
125+
children: nil,
126+
underlyingType: underlyingType,
127+
isObject: false,
128+
}
129+
}
130+
131+
// 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
132+
func (node *jsonNode) InsertOrUpdate(toInsert *jsonNode, location int) error {
133+
node.validateNode()
134+
if node.children != nil {
135+
return fmt.Errorf("node is a terminal primitive value %v, primitive values cannot have children", *node)
136+
}
137+
138+
// validInsertions are characterized by inserting into a struct at the correct type of
139+
validInsert := (getStructFieldType(node.underlyingType, location) == toInsert.underlyingType && location < len(node.children)) ||
140+
(node.underlyingType == toInsert.underlyingType && location <= len(node.children))
141+
142+
if validInsert {
143+
switch location {
144+
case len(node.children):
145+
node.children = append(node.children, toInsert)
146+
default:
147+
node.children = append(append(node.children[:location], toInsert), node.children[location:]...)
148+
}
149+
150+
return nil
151+
}
152+
153+
return fmt.Errorf("the insertion for %v index %d was invalid", *node, location)
154+
}
155+
156+
// NewPrimitiveFromValue constructs a new jsonNode from a primitive value
157+
func NewPrimitiveFromValue[T jsonPrimitives](key string, value T) *jsonNode {
158+
return &jsonNode{
159+
key: key,
160+
value: value,
161+
162+
children: nil,
163+
underlyingType: reflect.TypeOf(value),
164+
isObject: false,
165+
}
166+
}
167+
168+
// getStructFieldType fetches the field type of a struct given its index
169+
func getStructFieldType(structType reflect.Type, index int) reflect.Type {
170+
if structType.Kind() != reflect.Struct {
171+
return nil
172+
}
173+
174+
return structType.FieldByIndex([]int{index}).Type
175+
}

0 commit comments

Comments
 (0)