fspl/entity/meta.go

130 lines
3.7 KiB
Go

package entity
import "fmt"
import "strings"
import "unicode"
import "unicode/utf8"
import "path/filepath"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
// Metadata represents a module metadata file.
type Metadata struct {
Pos errors.Position
UUID uuid.UUID
Dependencies []*Dependency
}
func (this *Metadata) Position () errors.Position { return this.Pos }
func (this *Metadata) String () string {
out := fmt.Sprint(Quote(this.UUID.String()))
for _, dependency := range this.Dependencies {
out += fmt.Sprint("\n", dependency)
}
return out
}
// Directive is a declaration within a module metadata file.
type Directive interface {
fmt.Stringer
// Position returns the position of the directive within its metadata
// file.
Position () errors.Position
directive()
}
var _ Directive = &Dependency { }
// Dependency is a metadata dependency listing.
type Dependency struct {
Pos errors.Position
Address Address
Nickname string
}
func (*Dependency) directive () { }
func (this *Dependency) Position () errors.Position { return this.Pos }
func (this *Dependency) String () string {
out := fmt.Sprint("+ ", this.Address)
if this.Nickname != "" {
out += fmt.Sprint(" ", this.Nickname)
}
return out
}
// Address is the address of a unit.
type Address string
func (addr Address) String () string {
return Quote(string(addr))
}
// SourceFile returns the FSPL source file associated with the address. If the
// address does not point to an FSPL source file, it returns "", false.
func (addr Address) SourceFile () (string, bool) {
ext := filepath.Ext(string(addr))
if ext == ".fspl" {
return filepath.Clean(string(addr)), true
} else {
return "", false
}
}
// Module returns the module associated with the address. If the address does
// does not point to a module, it returns "", false.
func (addr Address) Module () (string, bool) {
path := string(addr)
switch {
case filepath.Base(path) == "fspl.mod":
return filepath.Dir(path), true
case filepath.Ext(path) == "" || filepath.Ext(path) == ".":
return filepath.Clean(path), true
default:
return "", false
}
}
// Nickname automatically generates a nickname from the address, which is a
// valid Ident token. On failure "", false is returned. The nickname is
// generated according to the folowing rules:
//
// - If the name contains at least one dot, the last dot and everything after
// it is removed
// - All non-letter, non-numeric characters are removed, and any letters that
// were directly after them are converted to uppercase
// - All numeric digits at the start of the string are removed
// - The first character is converted to lowercase
func (addr Address) Nickname () (string, bool) {
// get the normalized basename with no extension
base := filepath.Base(string(addr))
base = strings.TrimSuffix(base, filepath.Ext(base))
// remove all non-letter, non-digit characters and convert to camel case
nickname := ""
uppercase := false
for _, char := range base {
if unicode.IsLetter(char) || unicode.IsDigit(char) {
if uppercase {
uppercase = false
char = unicode.ToUpper(char)
}
nickname += string(char)
} else {
uppercase = true
}
}
// remove numeric digits
for len(nickname) > 0 {
char, size := utf8.DecodeRuneInString(nickname)
nickname = nickname[size:]
if unicode.IsLetter(char) {
// lowercase the first letter
nickname = string(unicode.ToLower(char)) + nickname
break
}
}
return nickname, nickname != ""
}
// UUID automatically generates a UUID from the address. It is the MD5 hash of
// the basename, with a space of the zero UUID.
func (addr Address) UUID () uuid.UUID {
return uuid.NewMD5(uuid.UUID { }, []byte(filepath.Base(string(addr))))
}