diff --git a/decoder.go b/decoder.go new file mode 100644 index 0000000..2f320cb --- /dev/null +++ b/decoder.go @@ -0,0 +1,71 @@ +package ini + +import ( + "bytes" + "io" + "strings" + + "asciigoat.org/core" + "asciigoat.org/core/reflection" + "asciigoat.org/ini/parser" +) + +// Decoder ... +type Decoder struct { + io.Closer + + out *reflection.Reflection + p *parser.Parser + queue []*token +} + +// Decode ... +func (dec *Decoder) Decode(v any) error { + defer dec.Close() + + r, err := reflection.New(v) + switch e := err.(type) { + case *reflection.InvalidUnmarshalError: + // customize error + e.Prefix = "ini" + e.Method = "Decode" + case nil: + // good reflection. Go! + dec.out = r + err = dec.p.Run() + } + + return err +} + +// NewDecoder creates a Decoder using the provided [io.Reader] +// as source +func NewDecoder(r io.Reader) *Decoder { + rc := core.NewReadCloser(r) + switch { + case rc == nil: + return nil + default: + dec := &Decoder{ + p: parser.NewParser(rc), + Closer: rc, + } + + // callbacks + dec.p.OnToken = dec.parserOnToken + dec.p.OnError = dec.parserOnError + + return dec + } +} + +// NewDecoderBytes creates a Decoder using the provided bytes array +// as source +func NewDecoderBytes(b []byte) *Decoder { + return NewDecoder(bytes.NewReader(b)) +} + +// NewDecoderString creates a Decoder over a provided string of data +func NewDecoderString(s string) *Decoder { + return NewDecoder(strings.NewReader(s)) +} diff --git a/decoder_error.go b/decoder_error.go new file mode 100644 index 0000000..e493e9c --- /dev/null +++ b/decoder_error.go @@ -0,0 +1,32 @@ +package ini + +import ( + "errors" + "log" + + "asciigoat.org/core/lexer" +) + +var ( + errInvalidToken = errors.New("invalid token") +) + +func newError(pos lexer.Position, content, hint string, err error) *lexer.Error { + return &lexer.Error{ + Line: pos.Line, + Column: pos.Column, + Content: content, + Hint: hint, + Err: err, + } +} +func (*Decoder) newErrInvalidToken(t *token) *lexer.Error { + return newError(t.pos, t.value, "", errInvalidToken) +} + +// parserOnError is the callback for lexer errors +func (*Decoder) parserOnError(pos lexer.Position, content string, err error) error { + log.Printf("%s: %s %s: %q: %v", "ini", pos, "error:", content, err) + + return newError(pos, content, "", err) +} diff --git a/decoder_token.go b/decoder_token.go new file mode 100644 index 0000000..21bcd9e --- /dev/null +++ b/decoder_token.go @@ -0,0 +1,144 @@ +package ini + +import ( + "fmt" + "log" + + "asciigoat.org/core/lexer" + "asciigoat.org/ini/parser" +) + +type token struct { + pos lexer.Position + typ parser.TokenType + value string +} + +func (t token) String() string { + return fmt.Sprintf("%s %s: %q", t.pos, t.typ, t.value) +} + +// queueValue extracts the value of element on the queue if the type matches. +func (dec *Decoder) queueValue(idx int, typ parser.TokenType) (string, bool) { + switch { + case idx < 0 || idx >= len(dec.queue): + // out of range + return "", false + case dec.queue[idx].typ != typ: + // wrong type + return "", false + default: + // match + return dec.queue[idx].value, true + } +} + +// queueReset removes all tokens from the queue +func (dec *Decoder) queueReset() { + dec.queue = dec.queue[:0] +} + +// queueType tells if the specified element on the queue is of the required type. +func (dec *Decoder) queueType(idx int, typ parser.TokenType) bool { + _, ok := dec.queueValue(idx, typ) + return ok +} + +// queueDepth confirms the current depth of the queue +func (dec *Decoder) queueDepth(depth int) bool { + return len(dec.queue) == depth +} + +// queueDepthType confirms the current depth of the queue and the type of the last +// element. +func (dec *Decoder) queueDepthType(depth int, typ parser.TokenType) bool { + if dec.queueDepth(depth) { + return dec.queueType(depth-1, typ) + } + return false +} + +// typeOK tells if a token of the specified type is acceptable +// at this time. +func (dec *Decoder) typeOK(typ parser.TokenType) bool { + switch typ { + case parser.TokenSectionStart: + return dec.queueDepth(0) + case parser.TokenSectionName: + return dec.queueDepthType(1, parser.TokenSectionStart) + case parser.TokenSectionSubname: + return dec.queueDepthType(2, parser.TokenSectionName) + case parser.TokenSectionEnd: + return dec.queueType(1, parser.TokenSectionName) + case parser.TokenFieldKey: + return dec.queueDepth(0) + case parser.TokenFieldValue: + return dec.queueDepthType(1, parser.TokenFieldKey) + case parser.TokenComment: + panic("unreachable") + default: + return false + } +} + +// execute is called after each acceptable token is appended to the queue +func (dec *Decoder) execute() error { + if l := len(dec.queue); l > 0 { + // based on the type of the last element + switch dec.queue[l-1].typ { + case parser.TokenSectionEnd: + name1, _ := dec.queueValue(1, parser.TokenSectionName) + name2, ok2 := dec.queueValue(2, parser.TokenSectionSubname) + defer dec.queueReset() + + return dec.executeSection(name1, name2, ok2) + case parser.TokenFieldValue: + key, _ := dec.queueValue(0, parser.TokenFieldKey) + value, _ := dec.queueValue(1, parser.TokenFieldValue) + defer dec.queueReset() + + return dec.executeField(key, value) + } + } + + return nil +} + +// revive:disable:flag-parameter + +func (*Decoder) executeSection(key, id string, hasID bool) error { + // revive:enable:flag-parameter + + if hasID { + log.Printf("%s: %s%s[%q]: %q", "ini", "", "section", key, id) + } else { + log.Printf("%s: %s%s[%q]", "ini", "", "section", key) + } + + return nil +} + +func (*Decoder) executeField(key, value string) error { + log.Printf("%s: %s%s[%q]: %q", "ini", " ", "field", key, value) + return nil +} + +// parserOnToken is the callback from the parser +func (dec *Decoder) parserOnToken(pos lexer.Position, typ parser.TokenType, value string) error { + var err error + + t := &token{pos, typ, value} + switch { + case typ == parser.TokenComment: + // ignore comments + case dec.typeOK(typ): + // acceptable token + dec.queue = append(dec.queue, t) + err = dec.execute() + default: + // unacceptable + err = dec.newErrInvalidToken(t) + } + + return err +}