// 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) }