flabk/pkg/asld/asld.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)
}