Initial commit

This commit is contained in:
Sasha Koshka 2024-08-31 02:31:29 -04:00
commit 9e79073e0f
3 changed files with 242 additions and 0 deletions

41
README.md Normal file
View File

@ -0,0 +1,41 @@
# ASV
This module allows encoding and decoding data using ASV (ASCII-separated
values). Data is organized into a hierarchy of files, groups, records, and
units, separated by ASCII FS, GS, RS, and US respectively.
An example stream of data...
```
record[US]one[RS]record[US]two[GS]record one in group two[FS]record one in file two
```
... Is parsed into:
```
Collection {
File {
Group {
Record {
"record",
"one",
},
Record {
"record",
"two",
},
},
Group {
Record {
"record one in group two",
},
},
},
File {
Group {
Record {
"record one in file two",
},
},
},
}
```
Although it uses ASCII control characters, this module operates on UTF-8 text.

198
asv.go Normal file
View File

@ -0,0 +1,198 @@
// Package asv provides encoding and decoding for ASCII separated values.
package asv
import "io"
import "bufio"
import "strings"
// Collection is composed of Files.
type Collection []File
// File is an element of a Collection, and is composed of Groups.
type File []Group
// Groups is an element of a File, and is composed of Records.
type Group []Record
// Record is an element of a Group, and is composed of Units.
type Record []Unit
// Unit is an element of a record.
type Unit string
const (
Escape rune = '\x1B'
FileSeparator rune = '\x1C'
GroupSeparator rune = '\x1D'
RecordSeparator rune = '\x1E'
UnitSeparator rune = '\x1F'
)
// IsSeparator returns true if the given rune is a separator.
func IsSeparator (r rune) bool {
return r >= FileSeparator && r <= UnitSeparator
}
// Decoder decodes ASV values.
type Decoder struct {
reader *bufio.Reader
}
// NewDecoder creates a new Decoder that reads from the specified io.Reader.
func NewDecoder (reader io.Reader) *Decoder {
return &Decoder {
reader: bufio.NewReader(reader),
}
}
// ReadGroup reads records until a separator higher than or equal to
// GroupSeparator is encountered, or an EOF is encountered. The function will
// then return the resulting Group, as well as the separator that caused it to
// terminate. On EOF, it will return 0 for next and io.EOF for err.
func (this *Decoder) ReadGroup () (group Group, next rune, err error) {
for {
record, next, err := this.ReadRecord()
group = append(group, record)
if err != nil { return group, 0, err }
if next >= GroupSeparator {
return group, next, nil
}
}
}
// ReadRecord reads units until a separator higher than or equal to
// RecordSeparator is encountered, or an EOF is encountered. The function will
// then return the resulting Record, as well as the separator that caused it to
// terminate. On EOF, it will return 0 for next and io.EOF for err.
func (this *Decoder) ReadRecord () (record Record, next rune, err error) {
for {
unit, next, err := this.ReadUnit()
record = append(record, unit)
if err != nil { return record, 0, err }
if next >= RecordSeparator {
return record, next, nil
}
}
}
// ReadUnit reads characters until a separator higher than or equal to
// UnitSeparator is encountered, or an EOF is encountered. The function will
// then return the resulting Unit, as well as the separator that caused it to
// terminate. On EOF, it will return 0 for next and io.EOF for err.
//
// If Escape is encountered before any character, that character will be
// included in the unit's string literally, minus the Escape.
func (this *Decoder) ReadUnit () (unit Unit, next rune, err error) {
esc := false
str := strings.Builder { }
for {
char, _, err := this.reader.ReadRune()
if err != nil { return Unit(str.String()), 0, err }
if esc {
esc = false
} else if char >= FileSeparator && char <= UnitSeparator {
return Unit(str.String()), char, nil
}
str.WriteRune(char)
}
}
// Encoder encodes ASV values. It keeps track of the last entity that was
// written, and inserts separators where necessary. After all data has been
// written, the client should call the Flush method to guarantee all data has
// been forwarded to the underlying io.Writer.
type Encoder struct {
writer *bufio.Writer
lastEntity rune
}
// WriteCollection writes a collection to the underlying io.Writer.
func (this *Encoder) WriteCollection (collection Collection) (n int, err error) {
for _, file := range collection {
nn, err := this.WriteFile(file)
n += nn
if err != nil { return n, err }
}
return n, nil
}
// WriteFile writes a file to the underlying io.Writer.
func (this *Encoder) WriteFile (file File) (n int, err error) {
nn, err := this.begin(FileSeparator)
n += nn
if err != nil { return n, err }
defer this.end(FileSeparator)
for _, group := range file {
nn, err := this.WriteGroup(group)
n += nn
if err != nil { return n, err }
}
return n, nil
}
// WriteGroup writes a group to the underlying io.Writer.
func (this *Encoder) WriteGroup (group Group) (n int, err error) {
nn, err := this.begin(GroupSeparator)
n += nn
if err != nil { return n, err }
defer this.end(GroupSeparator)
for _, record := range group {
nn, err := this.WriteRecord(record)
n += nn
if err != nil { return n, err }
}
return n, nil
}
// WriteRecord writes a record to the underlying io.Writer.
func (this *Encoder) WriteRecord (record Record) (n int, err error) {
nn, err := this.begin(RecordSeparator)
n += nn
if err != nil { return n, err }
defer this.end(RecordSeparator)
for _, unit := range record {
nn, err := this.WriteUnit(unit)
n += nn
if err != nil { return n, err }
}
return n, nil
}
// WriteUnit writes a unit to the underlying io.Writer. All separators, as well
// as Escape, are prepended with an Escape.
func (this *Encoder) WriteUnit (unit Unit) (n int, err error) {
nn, err := this.begin(UnitSeparator)
n += nn
if err != nil { return n, err }
defer this.end(UnitSeparator)
for _, char := range unit {
if IsSeparator(char) || char == Escape {
nn, _ := this.writer.WriteRune(Escape)
n += nn
}
nn, err := this.writer.WriteRune(char)
n += nn
if err != nil { return n, err }
}
return n, nil
}
func (this *Encoder) begin (separator rune) (n int, err error) {
if this.lastEntity == UnitSeparator {
nn, err := this.writer.WriteRune(separator)
n += nn
if err != nil { return n, err }
}
return n, nil
}
func (this *Encoder) end (separator rune) {
this.lastEntity = separator
}
// Flush writes any buffered data to the underlying io.Writer.
func (this *Encoder) Flush () error {
return this.writer.Flush()
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.tebibyte.media/sashakoshka/go-asv
go 1.22.2