306 lines
6.1 KiB
Go
306 lines
6.1 KiB
Go
|
// Package asld handles JSON-LD for asflab
|
||
|
//
|
||
|
// This will not go well
|
||
|
package asld
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
tagName = "asld"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrNoMatching = errors.New("could not find matching")
|
||
|
ErrSyntaxError = errors.New("syntax error")
|
||
|
ErrEntryWithoutValue = errors.New("entry without value")
|
||
|
)
|
||
|
|
||
|
// assigned by init
|
||
|
var (
|
||
|
byByte map[byte]Symbol
|
||
|
symbolsRaw []byte
|
||
|
iriPrefixes [][]byte
|
||
|
openSymbols []Symbol
|
||
|
openRaw []byte
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
byByte = map[byte]Symbol{}
|
||
|
for _, symbol := range symbols {
|
||
|
byByte[symbol.self] = symbol
|
||
|
}
|
||
|
symbolsRaw = make([]byte, len(symbols))
|
||
|
for index, symbol := range symbols {
|
||
|
symbolsRaw[index] = symbol.self
|
||
|
}
|
||
|
iriPrefixesStrings := []string{
|
||
|
// Currently only doing https
|
||
|
"https://",
|
||
|
}
|
||
|
iriPrefixes = make([][]byte, len(iriPrefixesStrings))
|
||
|
for index, prefix := range iriPrefixesStrings {
|
||
|
iriPrefixes[index] = []byte(prefix)
|
||
|
}
|
||
|
openSymbolEnums := []int{
|
||
|
symbolOpenParen, symbolOpenArray, symbolString,
|
||
|
}
|
||
|
openSymbols = make([]Symbol, len(openSymbolEnums))
|
||
|
for index, enum := range openSymbolEnums {
|
||
|
openSymbols[index] = symbols[enum]
|
||
|
}
|
||
|
openRaw = _map(openSymbols, func(s Symbol) byte {
|
||
|
return s.self
|
||
|
})
|
||
|
}
|
||
|
|
||
|
type Symbol struct {
|
||
|
self byte
|
||
|
closer byte
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
symbolOpenParen = iota
|
||
|
symbolClosedParen
|
||
|
symbolOpenArray
|
||
|
symbolClosedArray
|
||
|
symbolString
|
||
|
symbolColon
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
statusOK = iota
|
||
|
statusError
|
||
|
statusWalkerNotAffected
|
||
|
)
|
||
|
|
||
|
type status int
|
||
|
type walkerStatus struct {
|
||
|
walker
|
||
|
status
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
symbols = []Symbol{
|
||
|
symbolOpenParen: {
|
||
|
self: '{',
|
||
|
closer: '}',
|
||
|
},
|
||
|
symbolClosedParen: {
|
||
|
self: '}',
|
||
|
},
|
||
|
symbolOpenArray: {
|
||
|
self: '[',
|
||
|
closer: ']',
|
||
|
},
|
||
|
symbolClosedArray: {
|
||
|
self: ']',
|
||
|
},
|
||
|
symbolString: {
|
||
|
self: '"',
|
||
|
closer: '"',
|
||
|
},
|
||
|
symbolColon: {
|
||
|
self: ':',
|
||
|
},
|
||
|
}
|
||
|
)
|
||
|
|
||
|
func in[T comparable](this []T, has T) bool {
|
||
|
for _, elem := range this {
|
||
|
if elem == has {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func _map[T any, V any](v []T, f func(T) V) []V {
|
||
|
output := make([]V, len(v))
|
||
|
for index, elem := range v {
|
||
|
output[index] = f(elem)
|
||
|
}
|
||
|
return output
|
||
|
}
|
||
|
|
||
|
func isIRI(v []byte) bool {
|
||
|
for _, prefix := range iriPrefixes {
|
||
|
if bytes.Equal(v, prefix) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func arrayMembers(w walker) ([]walker, error) {
|
||
|
var (
|
||
|
members = []walker{}
|
||
|
final bool
|
||
|
)
|
||
|
|
||
|
elements, ok := w.SliceInner()
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("%s %w", string(w.content[w.position]), ErrNoMatching)
|
||
|
}
|
||
|
|
||
|
for !final {
|
||
|
elem, err := elements.CommaOrEnd()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("comma or end %s: %w", string(elem.content), err)
|
||
|
}
|
||
|
final = elem.status == statusWalkerNotAffected
|
||
|
value := elem.walker.Until().Reset()
|
||
|
if !final {
|
||
|
// Incremenet elem.walker.position here so
|
||
|
// that it skips over the comma that we're
|
||
|
// on right now. as there's not really anything
|
||
|
// valid to stop on in arrays aside from them
|
||
|
// so we can't just call ToNext() later.
|
||
|
elem.walker.position++
|
||
|
} else {
|
||
|
value = elem.walker
|
||
|
}
|
||
|
elements = elem.walker.Sub()
|
||
|
members = append(members, value)
|
||
|
}
|
||
|
|
||
|
return members, nil
|
||
|
}
|
||
|
|
||
|
func mapMembers(w walker) (map[string]walker, error) {
|
||
|
var (
|
||
|
members = map[string]walker{}
|
||
|
lastLine bool
|
||
|
)
|
||
|
elements, ok := w.SliceInner()
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("%s %w", string(w.content[w.position]), ErrNoMatching)
|
||
|
}
|
||
|
|
||
|
for !lastLine {
|
||
|
lineInfo, err := elements.CommaOrEnd()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("comma or end: %s: %w", string(elements.content), err)
|
||
|
}
|
||
|
lastLine = lineInfo.status == statusWalkerNotAffected
|
||
|
line := lineInfo.walker
|
||
|
if !lastLine {
|
||
|
line = line.Until()
|
||
|
}
|
||
|
|
||
|
name, ok := line.Reset().ToOrStay('"')
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
nameString, ok := name.SliceInner()
|
||
|
if !ok {
|
||
|
// TODO: maybe these should have global position
|
||
|
return nil, fmt.Errorf("%s %w", string(name.Current()), ErrNoMatching)
|
||
|
}
|
||
|
|
||
|
// We know this is OK because the above SliceInner called it
|
||
|
name, _ = name.To('"')
|
||
|
|
||
|
wNext, ok := name.Next()
|
||
|
if !ok && wNext.Current() != ':' && isIRI(nameString.content) {
|
||
|
panic("IRI expansion not implemented")
|
||
|
} else if !ok || wNext.Current() != ':' {
|
||
|
return nil, fmt.Errorf("%s: %w", string(nameString.content), ErrEntryWithoutValue)
|
||
|
}
|
||
|
|
||
|
value, ok := wNext.Next()
|
||
|
if !ok {
|
||
|
if value.position < value.len-1 {
|
||
|
value.position++
|
||
|
value = value.Sub()
|
||
|
} else {
|
||
|
return nil, fmt.Errorf("non-IRI %s: %w", string(nameString.content), ErrEntryWithoutValue)
|
||
|
}
|
||
|
}
|
||
|
elements = lineInfo.walker.Sub()
|
||
|
members[string(nameString.content)] = value
|
||
|
}
|
||
|
|
||
|
return members, nil
|
||
|
}
|
||
|
|
||
|
func unmarshalMap(out reflect.Value, w walker) error {
|
||
|
members, err := mapMembers(w)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("getting members: %w", err)
|
||
|
}
|
||
|
|
||
|
outType := out.Type()
|
||
|
// Deconstruct the struct fields
|
||
|
for index := 0; index < out.NumField(); index++ {
|
||
|
field := out.Field(index)
|
||
|
fType := outType.Field(index)
|
||
|
|
||
|
tagInfo := fType.Tag.Get(tagName)
|
||
|
// TODO: support expandible/collapsible/whatever I name it
|
||
|
// and omitempty probably
|
||
|
tagParts := strings.Split(tagInfo, ",")
|
||
|
name := tagParts[0]
|
||
|
if tagInfo == "" || name == "" {
|
||
|
name = fType.Name
|
||
|
}
|
||
|
// mimic encoding/json behavior
|
||
|
if name == "-" && len(tagParts) == 1 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
wField, exists := members[name]
|
||
|
if !exists {
|
||
|
continue
|
||
|
}
|
||
|
setValue(fType, field, wField)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func setValue(fType reflect.StructField, field reflect.Value, w walker) error {
|
||
|
switch field.Kind() {
|
||
|
case reflect.String:
|
||
|
if w.content[0] != '"' {
|
||
|
return fmt.Errorf("%s is not a string", string(w.content))
|
||
|
}
|
||
|
field.SetString(w.String())
|
||
|
default:
|
||
|
panic("not implemented")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func unmarshal(out reflect.Value, w walker) error {
|
||
|
switch out.Kind() {
|
||
|
case reflect.Struct:
|
||
|
return unmarshalMap(out, w)
|
||
|
case reflect.Array, reflect.Slice:
|
||
|
// do array stuff here
|
||
|
default:
|
||
|
panic(out.Kind().String() + " not yet supported")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (w walker) Reset() walker {
|
||
|
w.position = 0
|
||
|
return w
|
||
|
}
|
||
|
|
||
|
func Unmarshal[T any](data []byte) (T, error) {
|
||
|
tPtr := new(T)
|
||
|
tValue := reflect.Indirect(reflect.ValueOf(tPtr))
|
||
|
w := newWalker(data)
|
||
|
|
||
|
return *tPtr, unmarshal(tValue, w)
|
||
|
}
|