fully implemented profile editing

This commit is contained in:
2025-10-11 16:49:17 -06:00
parent 297c1b9157
commit 9d2c57815e
10 changed files with 545 additions and 43 deletions
+34
View File
@@ -125,6 +125,7 @@ func (ci *ContactInfo) Save() (bool, error) {
if err != nil {
return false, err
}
contactCache.Add(ci.ContactId, ci)
} else {
res, err := amdb.NamedExec(`INSERT INTO contacts (given_name, family_name, middle_init, prefix, suffix, company, addr1,
addr2, locality, region, pcode, country, phone, fax, mobile, email, pvt_addr, pvt_phone, pvt_fax,
@@ -151,6 +152,39 @@ func (ci *ContactInfo) Save() (bool, error) {
return emailChange, nil
}
// Clone makes a copy of the ContactInfo.
func (ci *ContactInfo) Clone() *ContactInfo {
newstr := ContactInfo{
ContactId: ci.ContactId,
GivenName: ci.GivenName,
FamilyName: ci.FamilyName,
MiddleInit: ci.MiddleInit,
Prefix: ci.Prefix,
Suffix: ci.Suffix,
Company: ci.Company,
Addr1: ci.Addr1,
Addr2: ci.Addr2,
Locality: ci.Locality,
Region: ci.Region,
PostalCode: ci.PostalCode,
Country: ci.Country,
Phone: ci.Phone,
Fax: ci.Fax,
Mobile: ci.Mobile,
Email: ci.Mobile,
PrivateAddr: ci.PrivateAddr,
PrivatePhone: ci.PrivatePhone,
PrivateFax: ci.PrivateFax,
PrivateEmail: ci.PrivateEmail,
OwnerUid: ci.OwnerUid,
OwnerCommId: ci.OwnerCommId,
PhotoURL: ci.PhotoURL,
URL: ci.URL,
LastUpdate: ci.LastUpdate,
}
return &newstr
}
// contactCache is the cache for ContactInfo objects.
var contactCache *lru.TwoQueueCache = nil
+57
View File
@@ -0,0 +1,57 @@
/*
* Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// The database package contains database management and storage logic.
package database
import (
"time"
"git.erbosoft.com/amy/amsterdam/util"
)
// PasswordChangeRequest represents a temporary password change request.
type PasswordChangeRequest struct {
Uid int32
Username string
Email string
Authentication int32
Expires time.Time
}
// passwordRequests contains a map of password change requests currently managed.
var passwordRequests map[int32]*PasswordChangeRequest = make(map[int32]*PasswordChangeRequest)
/* AmNewPasswordChangeRequest creates a new password change request and enrolls it.
* Parameters:
* uid - The UID of the user.
* username - The user name of the user.
* email - The E-mail address of the user.
* Returns:
* Pointer to the new PasswordChangeRequest.
*/
func AmNewPasswordChangeRequest(uid int32, username string, email string) *PasswordChangeRequest {
rc := PasswordChangeRequest{Uid: uid, Username: username, Email: email,
Authentication: util.GenerateRandomConfirmationNumber(), Expires: time.Now().Add(time.Hour)}
passwordRequests[uid] = &rc
return &rc
}
/* AmGetPasswordChangeRequest retrieves the password change request for a UID.
* Parameters:
* uid - The UID to retrieve the request for.
* Returns:
* The PasswordChangeRequest pointer, or nil.
*/
func AmGetPasswordChangeRequest(uid int32) *PasswordChangeRequest {
rc := passwordRequests[uid]
if rc != nil {
delete(passwordRequests, uid)
}
return rc
}
+198 -33
View File
@@ -25,45 +25,39 @@ import (
log "github.com/sirupsen/logrus"
)
// PasswordChangeRequest represents a temporary password change request.
type PasswordChangeRequest struct {
Uid int32
Username string
Email string
Authentication int32
Expires time.Time
// UserPrefs represents the user's preferences in a table (one row per user).
type UserPrefs struct {
Uid int32 `db:"uid"`
TimeZoneID string `db:"tzid"`
LocaleID string `db:"localeid"`
}
// passwordRequests contains a map of password change requests currently managed.
var passwordRequests map[int32]*PasswordChangeRequest = make(map[int32]*PasswordChangeRequest)
// ReadLocale reads the locale out of the prefs, adjusting for Go use.
func (p *UserPrefs) ReadLocale() string {
return strings.Replace(p.LocaleID, "_", "-", -1)
}
/* AmNewPasswordChangeRequest creates a new password change request and enrolls it.
* Parameters:
* uid - The UID of the user.
* username - The user name of the user.
* email - The E-mail address of the user.
* Returns:
* Pointer to the new PasswordChangeRequest.
*/
func AmNewPasswordChangeRequest(uid int32, username string, email string) *PasswordChangeRequest {
rc := PasswordChangeRequest{Uid: uid, Username: username, Email: email,
Authentication: util.GenerateRandomConfirmationNumber(), Expires: time.Now().Add(time.Hour)}
passwordRequests[uid] = &rc
// WriteLocale writes the locale into the prefs, adjusting for backward compatibility.
func (p *UserPrefs) WriteLocale(loc string) {
p.LocaleID = strings.Replace(loc, "-", "_", -1)
}
// Clone duplicates the user preferences.
func (p *UserPrefs) Clone() *UserPrefs {
rc := *p
return &rc
}
/* AmGetPasswordChangeRequest retrieves the password change request for a UID.
* Parameters:
* uid - The UID to retrieve the request for.
* Returns:
* The PasswordChangeRequest pointer, or nil.
*/
func AmGetPasswordChangeRequest(uid int32) *PasswordChangeRequest {
rc := passwordRequests[uid]
if rc != nil {
delete(passwordRequests, uid)
// Save saves off the user preferences, replacing the prefs on the user if necessary.
func (p *UserPrefs) Save(u *User) error {
if u != nil && u.Uid != p.Uid {
return errors.New("internal mismatch of IDs")
}
return rc
_, err := amdb.NamedExec("UPDATE userprefs SET localeid = :localeid, tzid = :tzid WHERE uid = :uid", p)
if err == nil && u != nil {
u.prefs = p
}
return err
}
// User represents a user in the Amsterdam database.
@@ -85,6 +79,8 @@ type User struct {
PassReminder string `db:"passreminder"`
Description *string `db:"description"`
DOB *time.Time `db:"dob"`
flags *util.OptionSet
prefs *UserPrefs
}
// UserProperties represents a property entry for a user.
@@ -94,22 +90,44 @@ type UserProperties struct {
Data *string `db:"data"`
}
// User property indexes defined.
const (
UserPropFlags = int32(0) // "flags" user property
)
// Flag values for user property index UserPropFlags defined.
const (
UserFlagPicturesInPosts = uint(0)
UserFlagDisallowSetPhoto = uint(1)
UserFlagMassMailOptOut = uint(2)
)
// userCache is the cache for User objects.
var userCache *lru.TwoQueueCache = nil
// getUserMutex is a mutex on AmGetUser.
var getUserMutex sync.Mutex
// userPropCache is the cache for UserProperties objects.
var userPropCache *lru.Cache = nil
// getUserPropMutex is a mutex on AmGetUserProperty.
var getUserPropMutex sync.Mutex
// anonUid is the UID of the "anonymous" user.
var anonUid int32 = -1
// init initializes the user cache.
// init initializes the caches.
func init() {
var err error
userCache, err = lru.New2Q(100)
if err != nil {
panic(err)
}
userPropCache, err = lru.New(100)
if err != nil {
panic(err)
}
}
// ContactInfo returns the contact info structure for the user.
@@ -219,6 +237,83 @@ func (u *User) ChangePassword(password string, remoteIP string) error {
return err
}
// GetFlags retrieves the flags from the properties.
func (u *User) Flags() (*util.OptionSet, error) {
u.Mutex.Lock()
defer u.Mutex.Unlock()
if u.flags == nil {
s, err := AmGetUserProperty(u.Uid, UserPropFlags)
if err != nil {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("missing flags for user %d", u.Uid)
}
u.flags = util.OptionSetFromString(*s)
}
return u.flags, nil
}
// SaveFlags writes the flags to the database and stores them.
func (u *User) SaveFlags(f *util.OptionSet) error {
s := f.AsString()
u.Mutex.Lock()
defer u.Mutex.Unlock()
err := AmSetUserProperty(u.Uid, UserPropFlags, &s)
if err == nil {
u.flags = f
}
return err
}
// FlagValue returns the boolean value of one of the user flags.
func (u *User) FlagValue(ndx uint) bool {
f, err := u.Flags()
if err != nil {
log.Errorf("flag retrieval error for user %d: %v", u.Uid, err)
return false
}
return f.Get(ndx)
}
// Prefs returns the user's preferences record.
func (u *User) Prefs() (*UserPrefs, error) {
u.Mutex.Lock()
defer u.Mutex.Unlock()
if u.prefs == nil {
var dbdata []UserPrefs
err := amdb.Select(&dbdata, "SELECT * FROM userprefs WHERE uid = ?", u.Uid)
if err != nil {
return nil, err
}
if len(dbdata) != 1 {
return nil, fmt.Errorf("invalid preferences records for user %d", u.Uid)
}
u.prefs = &(dbdata[0])
}
return u.prefs, nil
}
/* SetProfileData sets the "profile" variables for this user.
* Parameters:
* reminder - Password reminder string.
* dob - Date of birth field.
* descr - Description string.
* Returns:
* Standard Go error status.
*/
func (u *User) SetProfileData(reminder string, dob *time.Time, descr *string) error {
u.Mutex.Lock()
defer u.Mutex.Unlock()
_, err := amdb.Exec("UPDATE users SET passreminder = ?, dob = ?, description = ? WHERE uid = ?", reminder, dob, descr, u.Uid)
if err == nil {
u.PassReminder = reminder
u.DOB = dob
u.Description = descr
}
return err
}
/* AmGetUser returns a reference to the specified user.
* Parameters:
* uid - The UID of the user.
@@ -545,3 +640,73 @@ func AmCreateNewUser(username string, password string, reminder string, dob *tim
ar = AmNewAudit(AuditAccountCreated, user.Uid, remoteIP)
return user, nil
}
func internalGetProp(uid int32, ndx int32) (*UserProperties, error) {
var err error = nil
key := fmt.Sprintf("%d:%d", uid, ndx)
getUserPropMutex.Lock()
defer getUserPropMutex.Unlock()
rc, ok := userPropCache.Get(key)
if !ok {
var dbdata []UserProperties
err = amdb.Select(&dbdata, "SELECT * from propuser WHERE uid = ? AND ndx = ?", uid, ndx)
if err != nil {
return nil, err
}
if len(dbdata) == 0 {
return nil, nil
}
if len(dbdata) > 1 {
return nil, fmt.Errorf("AmGetUserProperty(%d): too many responses(%d)", uid, len(dbdata))
}
rc = &(dbdata[0])
userPropCache.Add(key, rc)
}
return rc.(*UserProperties), nil
}
/* AmGetUserProperty retrieves the value of a user property.
* Parameters:
* uid - The UID of the user to get the property for.
* ndx - The index of the property to retrieve.
* Returns:
* Value of the property string.
* Standard Go error status.
*/
func AmGetUserProperty(uid int32, ndx int32) (*string, error) {
p, err := internalGetProp(uid, ndx)
if err != nil {
return nil, err
}
return p.Data, nil
}
/* AmSetUserProperty sets the value of a user property.
* Parameters:
* uid - The UID of the user to set the property for.
* ndx - The index of the property to set.
* val - The new value of the property.
* Returns:
* Standard Go error status.
*/
func AmSetUserProperty(uid int32, ndx int32, val *string) error {
p, err := internalGetProp(uid, ndx)
if err != nil {
return err
}
getUserPropMutex.Lock()
defer getUserPropMutex.Unlock()
if p != nil {
_, err = amdb.Exec("UPDATE propuser SET data = ? WHERE uid = ? AND ndx = ?", val, uid, ndx)
if err == nil {
p.Data = val
}
} else {
prop := UserProperties{Uid: uid, Index: ndx, Data: val}
_, err := amdb.NamedExec("INSERT INTO propuser (uid, ndx, data) VALUES(:uid, :ndx, :data)", prop)
if err == nil {
userPropCache.Add(fmt.Sprintf("%d:%d", uid, ndx), prop)
}
}
return err
}
+1
View File
@@ -6,6 +6,7 @@ require (
github.com/CloudyKit/jet/v6 v6.3.1
github.com/alexflint/go-arg v1.6.0
github.com/biter777/countries v1.7.5
github.com/bits-and-blooms/bitset v1.24.0
github.com/go-sql-driver/mysql v1.9.3
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/golang-lru v1.0.2
+2
View File
@@ -10,6 +10,8 @@ github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+W
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+1
View File
@@ -53,6 +53,7 @@ func setupEcho() *echo.Echo {
e.POST("/verify", ui.AmWrap(VerifyEMail))
e.GET("/passrecovery/:uid/:auth", ui.AmWrap(PasswordRecovery))
e.GET("/profile", ui.AmWrap(EditProfileForm))
e.POST("/profile", ui.AmWrap(EditProfile))
return e
}
+49 -7
View File
@@ -47,6 +47,7 @@ type Dialog struct {
Action string `yaml:"action"`
Instructions string `yaml:"instructions,omitempty"`
Fields []DialogItem `yaml:"fields"`
fldmap map[string]*DialogItem
}
// VRange is used as a return type for ValueRange.
@@ -68,11 +69,13 @@ func AmLoadDialog(name string) (*Dialog, error) {
var d Dialog
err = yaml.Unmarshal(b, &d)
if err == nil {
// "nil-patch" certain fields
// "nil-patch" certain fields and create the fast-lookup map
if d.MenuSelector == "" {
d.MenuSelector = "nochange"
}
d.fldmap = make(map[string]*DialogItem)
for i, fld := range d.Fields {
d.fldmap[fld.Name] = &(d.Fields[i])
if fld.Type == "button" && fld.Param == "" {
d.Fields[i].Param = "blue"
}
@@ -106,6 +109,17 @@ func (fld *DialogItem) IsChecked() bool {
return false
}
// SetChecked sets the value of a checkbox.
func (fld *DialogItem) SetChecked(val bool) {
if fld.Type == "checkbox" {
if val {
fld.Value = "Y"
} else {
fld.Value = ""
}
}
}
// ValueInt returns the value of the field as an integer.
func (fld *DialogItem) ValueInt() (int, error) {
return strconv.Atoi(fld.Value)
@@ -134,6 +148,25 @@ func (fld *DialogItem) AsDate() *time.Time {
return nil
}
// SetDate sets the value of the dialog item as a date.
func (fld *DialogItem) SetDate(d *time.Time) {
if fld.Type == "date" {
dvs := make([]int, 3)
if d == nil {
dvs[0] = -1
dvs[1] = -1
dvs[2] = -1
fld.Value = ""
} else {
dvs[0] = int(d.Month()) - int(time.January) + 1
dvs[1] = d.Day()
dvs[2] = d.Year()
fld.Value = fmt.Sprintf("%04d%02d%02d", dvs[2], dvs[0], dvs[1])
}
fld.AuxData = dvs
}
}
// ValPtr returns the value of a field as a string pointer, or nil if the field is empty.
func (fld *DialogItem) ValPtr() *string {
if fld.Value == "" {
@@ -142,6 +175,20 @@ func (fld *DialogItem) ValPtr() *string {
return &fld.Value
}
// SetVal sets the value of a field from a string pointer.
func (fld *DialogItem) SetVal(p *string) {
if p == nil {
fld.Value = ""
} else {
fld.Value = *p
}
}
// IsEmpty returns true if the field is empty.
func (fld *DialogItem) IsEmpty() bool {
return len(fld.Value) == 0
}
/* Field returns a pointer to a dialog's field, given its name.
* Parameters:
* name - The name of the field to find.
@@ -149,12 +196,7 @@ func (fld *DialogItem) ValPtr() *string {
* Pointer to the field, or nil.
*/
func (d *Dialog) Field(name string) *DialogItem {
for i, f := range d.Fields {
if f.Name == name {
return &(d.Fields[i])
}
}
return nil
return d.fldmap[name]
}
/* Render sets up the rendering parameters to send this dialog to the output.
+1 -1
View File
@@ -17,7 +17,7 @@ fields:
value: ""
- type: "header"
name: "header1"
caption: "Name"
caption: "Password"
subcaption: "To change your password, enter a new password into the fields below."
- type: "password"
name: "pass1"
+135 -2
View File
@@ -11,8 +11,11 @@ package main
import (
"errors"
"net/url"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
)
/* EditProfileForm renders the Amsterdam profile editing form.
@@ -36,8 +39,138 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) {
dlg, err := ui.AmLoadDialog("profile")
if err == nil {
dlg.Field("tgt").Value = target
// TODO: load fields from current user
return dlg.Render(ctxt)
var ci *database.ContactInfo
ci, err = u.ContactInfo()
if err == nil {
var prefs *database.UserPrefs
prefs, err = u.Prefs()
if err == nil {
dlg.Field("remind").Value = u.PassReminder
dlg.Field("prefix").SetVal(ci.Prefix)
dlg.Field("first").Value = ci.GivenName
dlg.Field("mid").SetVal(ci.MiddleInit)
dlg.Field("last").Value = ci.FamilyName
dlg.Field("suffix").SetVal(ci.Suffix)
dlg.Field("company").SetVal(ci.Company)
dlg.Field("addr1").SetVal(ci.Addr1)
dlg.Field("addr2").SetVal(ci.Addr2)
dlg.Field("pvt_addr").SetChecked(ci.PrivateAddr)
dlg.Field("loc").SetVal(ci.Locality)
dlg.Field("reg").SetVal(ci.Region)
dlg.Field("pcode").SetVal(ci.PostalCode)
dlg.Field("country").SetVal(ci.Country)
dlg.Field("phone").SetVal(ci.Phone)
dlg.Field("mobile").SetVal(ci.Mobile)
dlg.Field("pvt_phone").SetChecked(ci.PrivatePhone)
dlg.Field("fax").SetVal(ci.Fax)
dlg.Field("pvt_fax").SetChecked(ci.PrivateFax)
dlg.Field("email").SetVal(ci.Email)
dlg.Field("pvt_email").SetChecked(ci.PrivateEmail)
dlg.Field("url").SetVal(ci.URL)
dlg.Field("dob").SetDate(u.DOB)
dlg.Field("descr").SetVal(u.Description)
// TODO: do something for user photo
dlg.Field("pic_in_post").SetChecked(u.FlagValue(database.UserFlagPicturesInPosts))
dlg.Field("no_mass_mail").SetChecked(u.FlagValue(database.UserFlagMassMailOptOut))
dlg.Field("locale").Value = prefs.ReadLocale()
dlg.Field("tz").Value = prefs.TimeZoneID
return dlg.Render(ctxt)
}
}
}
return ui.ErrorPage(ctxt, err)
}
func EditProfile(ctxt ui.AmContext) (string, any, error) {
u := ctxt.CurrentUser()
if u.IsAnon {
return ui.ErrorPage(ctxt, errors.New("you are not logged in"))
}
dlg, err := ui.AmLoadDialog("profile")
if err == nil {
dlg.LoadFromForm(ctxt)
target := dlg.Field("tgt").Value
if target == "" {
target = "/"
}
action := dlg.WhichButton(ctxt)
if action == "cancel" { // Cancel button pressed
return "redirect", target, nil
}
if action == "update" {
var ci *database.ContactInfo
ci, err = u.ContactInfo()
if err == nil {
var prefs *database.UserPrefs
emailChange := false
prefs, err = u.Prefs()
if err == nil && !(dlg.Field("pass1").IsEmpty() && dlg.Field("pass2").IsEmpty()) {
p1 := dlg.Field("pass1").Value
if p1 == dlg.Field("pass2").Value {
err = u.ChangePassword(p1, ctxt.RemoteIP())
} else {
err = errors.New("passwords do not match")
}
}
if err == nil {
nci := ci.Clone()
nci.Prefix = dlg.Field("prefix").ValPtr()
nci.GivenName = dlg.Field("first").Value
nci.MiddleInit = dlg.Field("mid").ValPtr()
nci.FamilyName = dlg.Field("last").Value
nci.Suffix = dlg.Field("suffix").ValPtr()
nci.Company = dlg.Field("company").ValPtr()
nci.Addr1 = dlg.Field("addr1").ValPtr()
nci.Addr2 = dlg.Field("addr2").ValPtr()
nci.PrivateAddr = dlg.Field("pvt_addr").IsChecked()
nci.Locality = dlg.Field("loc").ValPtr()
nci.Region = dlg.Field("reg").ValPtr()
nci.PostalCode = dlg.Field("pcode").ValPtr()
nci.Country = dlg.Field("country").ValPtr()
nci.Phone = dlg.Field("phone").ValPtr()
nci.Mobile = dlg.Field("mobile").ValPtr()
nci.PrivatePhone = dlg.Field("pvt_phone").IsChecked()
nci.Fax = dlg.Field("fax").ValPtr()
nci.PrivateFax = dlg.Field("pvt_fax").IsChecked()
nci.Email = dlg.Field("email").ValPtr()
nci.PrivateEmail = dlg.Field("pvt_email").IsChecked()
nci.URL = dlg.Field("url").ValPtr()
emailChange, err = nci.Save()
ci = nci
}
if err == nil {
nprefs := prefs.Clone()
nprefs.WriteLocale(dlg.Field("locale").Value)
nprefs.TimeZoneID = dlg.Field("tz").Value
err = nprefs.Save(u)
}
if err == nil {
var f *util.OptionSet
f, err = u.Flags()
if err == nil {
nf := f.Clone()
nf.Set(database.UserFlagPicturesInPosts, dlg.Field("pic_in_post").IsChecked())
nf.Set(database.UserFlagMassMailOptOut, dlg.Field("no_mass_mail").IsChecked())
err = u.SaveFlags(nf)
}
}
if err == nil {
err = u.SetProfileData(dlg.Field("remind").Value, dlg.Field("dob").AsDate(), dlg.Field("descr").ValPtr())
}
if err == nil {
if emailChange {
err = sendEmailConfirmationEmail(u, ci, ctxt.RemoteIP())
if err == nil {
return "redirect", "/verify?tgt=" + url.PathEscape(target), nil
}
} else {
return "redirect", target, nil
}
}
}
}
return dlg.RenderError(ctxt, "No known button click on POST to profile.")
}
return ui.ErrorPage(ctxt, err)
}
+67
View File
@@ -0,0 +1,67 @@
/*
* Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Package util contains utility definitions.
package util
import (
"strings"
"github.com/bits-and-blooms/bitset"
)
// optionAlphabet is the alphabet from which OptionSets serialize to and from strings.
const optionAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~"
// OptionSet is a bit set that can be persisted as a specially-constructed string.
type OptionSet struct {
bits *bitset.BitSet
}
// Get retrieves the value of a bit from the given set.
func (s *OptionSet) Get(ndx uint) bool {
return s.bits.Test(ndx)
}
// Set sets the value of a bit in the given set.
func (s *OptionSet) Set(ndx uint, v bool) {
if v {
s.bits = s.bits.Set(ndx)
} else {
s.bits = s.bits.Clear(ndx)
}
}
// AsString returns the option set's value as a string.
func (s *OptionSet) AsString() string {
var b strings.Builder
for i, e := s.bits.NextSet(0); e; i, e = s.bits.NextSet(i + 1) {
b.WriteByte(optionAlphabet[int(i)])
}
return b.String()
}
// Clone creates a clone of this OptionSet.
func (s *OptionSet) Clone() *OptionSet {
return &OptionSet{bits: s.bits.Clone()}
}
// NewOptionSet creates and returns an empty option set.
func NewOptionSet() *OptionSet {
return &OptionSet{bits: bitset.New(uint(len(optionAlphabet)))}
}
// OptionSetFromString converts a string into a corresponding OptionSet.
func OptionSetFromString(s string) *OptionSet {
bs := bitset.New(uint(len(optionAlphabet)))
for _, ch := range s {
bs = bs.Set(uint(strings.IndexRune(optionAlphabet, ch)))
}
return &OptionSet{bits: bs}
}